diff --git a/src/applications/almanac/constants/AlmanacDeviceStatus.php b/src/applications/almanac/constants/AlmanacDeviceStatus.php index 492e0d2a98..0bd27dde86 100644 --- a/src/applications/almanac/constants/AlmanacDeviceStatus.php +++ b/src/applications/almanac/constants/AlmanacDeviceStatus.php @@ -1,89 +1,110 @@ value = $value; return $status; } public function getValue() { return $this->value; } public function getName() { $name = $this->getDeviceStatusProperty('name'); if ($name === null) { $name = pht('Unknown Almanac Device Status ("%s")', $this->getValue()); } return $name; } public function getIconIcon() { return $this->getDeviceStatusProperty('icon.icon'); } public function getIconColor() { return $this->getDeviceStatusProperty('icon.color'); } public function isDisabled() { return ($this->getValue() === self::DISABLED); } public function hasStatusTag() { return ($this->getStatusTagIcon() !== null); } public function getStatusTagIcon() { return $this->getDeviceStatusProperty('status-tag.icon'); } public function getStatusTagColor() { return $this->getDeviceStatusProperty('status-tag.color'); } public static function getStatusMap() { $result = array(); foreach (self::newDeviceStatusMap() as $status_value => $ignored) { $result[$status_value] = self::newStatusFromValue($status_value); } return $result; } + public static function getActiveStatusList() { + $results = array(); + foreach (self::newDeviceStatusMap() as $status_value => $status) { + if (empty($status['disabled'])) { + $results[] = $status_value; + } + } + return $results; + } + + public static function getDisabledStatusList() { + $results = array(); + foreach (self::newDeviceStatusMap() as $status_value => $status) { + if (!empty($status['disabled'])) { + $results[] = $status_value; + } + } + return $results; + } + private function getDeviceStatusProperty($key, $default = null) { $map = self::newDeviceStatusMap(); $properties = idx($map, $this->getValue(), array()); return idx($properties, $key, $default); } private static function newDeviceStatusMap() { return array( self::ACTIVE => array( 'name' => pht('Active'), 'icon.icon' => 'fa-server', 'icon.color' => 'green', ), self::DISABLED => array( 'name' => pht('Disabled'), 'icon.icon' => 'fa-times', 'icon.color' => 'grey', 'status-tag.icon' => 'fa-times', 'status-tag.color' => 'indigo', + 'disabled' => true, ), ); } } diff --git a/src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php b/src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php index a3afaf3251..c4f6b823ee 100644 --- a/src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php +++ b/src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php @@ -1,30 +1,53 @@ isActive = $is_active; + return $this; + } + + public function getIsActive() { + return $this->isActive; + } + public function getAttachmentName() { return pht('Almanac Bindings'); } public function getAttachmentDescription() { return pht('Get Almanac bindings for the service.'); } public function willLoadAttachmentData($query, $spec) { $query->needProperties(true); - $query->needBindings(true); + + if ($this->getIsActive()) { + $query->needBindings(true); + } else { + $query->needActiveBindings(true); + } } public function getAttachmentForObject($object, $data, $spec) { $bindings = array(); - foreach ($object->getBindings() as $binding) { + + if ($this->getIsActive()) { + $service_bindings = $object->getActiveBindings(); + } else { + $service_bindings = $object->getBindings(); + } + + foreach ($service_bindings as $binding) { $bindings[] = $this->getAlmanacBindingDictionary($binding); } return array( 'bindings' => $bindings, ); } } diff --git a/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php b/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php index 266b281990..97672a2af0 100644 --- a/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php +++ b/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php @@ -1,65 +1,67 @@ getAlmanacPropertyFieldSpecifications(); $properties = array(); foreach ($object->getAlmanacProperties() as $key => $property) { $is_builtin = isset($builtins[$key]); $properties[] = array( 'key' => $key, 'value' => $property->getFieldValue(), 'builtin' => $is_builtin, ); } return $properties; } protected function getAlmanacBindingDictionary(AlmanacBinding $binding) { $interface = $binding->getInterface(); return array( 'id' => (int)$binding->getID(), 'phid' => $binding->getPHID(), 'properties' => $this->getAlmanacPropertyList($binding), 'interface' => $this->getAlmanacInterfaceDictionary($interface), 'disabled' => (bool)$binding->getIsDisabled(), ); } protected function getAlmanacInterfaceDictionary( AlmanacInterface $interface) { return array( 'id' => (int)$interface->getID(), 'phid' => $interface->getPHID(), 'address' => $interface->getAddress(), 'port' => (int)$interface->getPort(), 'device' => $this->getAlmanacDeviceDictionary($interface->getDevice()), 'network' => $this->getAlmanacNetworkDictionary($interface->getNetwork()), ); } protected function getAlmanacDeviceDictionary(AlmanacDevice $device) { return array( 'id' => (int)$device->getID(), 'phid' => $device->getPHID(), 'name' => $device->getName(), 'properties' => $this->getAlmanacPropertyList($device), + 'status' => $device->getStatus(), + 'disabled' => $device->isDisabled(), ); } protected function getAlmanacNetworkDictionary(AlmanacNetwork $network) { return array( 'id' => (int)$network->getID(), 'phid' => $network->getPHID(), 'name' => $network->getName(), ); } } diff --git a/src/applications/almanac/query/AlmanacBindingQuery.php b/src/applications/almanac/query/AlmanacBindingQuery.php index 5d51d2ba35..574967f7bb 100644 --- a/src/applications/almanac/query/AlmanacBindingQuery.php +++ b/src/applications/almanac/query/AlmanacBindingQuery.php @@ -1,133 +1,179 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withServicePHIDs(array $phids) { $this->servicePHIDs = $phids; return $this; } public function withDevicePHIDs(array $phids) { $this->devicePHIDs = $phids; return $this; } public function withInterfacePHIDs(array $phids) { $this->interfacePHIDs = $phids; return $this; } + public function withIsActive($active) { + $this->isActive = $active; + return $this; + } + public function newResultObject() { return new AlmanacBinding(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $bindings) { $service_phids = mpull($bindings, 'getServicePHID'); $device_phids = mpull($bindings, 'getDevicePHID'); $interface_phids = mpull($bindings, 'getInterfacePHID'); $services = id(new AlmanacServiceQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($service_phids) ->needProperties($this->getNeedProperties()) ->execute(); $services = mpull($services, null, 'getPHID'); $devices = id(new AlmanacDeviceQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($device_phids) ->needProperties($this->getNeedProperties()) ->execute(); $devices = mpull($devices, null, 'getPHID'); $interfaces = id(new AlmanacInterfaceQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($interface_phids) ->needProperties($this->getNeedProperties()) ->execute(); $interfaces = mpull($interfaces, null, 'getPHID'); foreach ($bindings as $key => $binding) { $service = idx($services, $binding->getServicePHID()); $device = idx($devices, $binding->getDevicePHID()); $interface = idx($interfaces, $binding->getInterfacePHID()); if (!$service || !$device || !$interface) { $this->didRejectResult($binding); unset($bindings[$key]); continue; } $binding->attachService($service); $binding->attachDevice($device); $binding->attachInterface($interface); } return $bindings; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'binding.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'binding.phid IN (%Ls)', $this->phids); } if ($this->servicePHIDs !== null) { $where[] = qsprintf( $conn, - 'servicePHID IN (%Ls)', + 'binding.servicePHID IN (%Ls)', $this->servicePHIDs); } if ($this->devicePHIDs !== null) { $where[] = qsprintf( $conn, - 'devicePHID IN (%Ls)', + 'binding.devicePHID IN (%Ls)', $this->devicePHIDs); } if ($this->interfacePHIDs !== null) { $where[] = qsprintf( $conn, - 'interfacePHID IN (%Ls)', + 'binding.interfacePHID IN (%Ls)', $this->interfacePHIDs); } + if ($this->isActive !== null) { + if ($this->isActive) { + $where[] = qsprintf( + $conn, + '(binding.isDisabled = 0) AND (device.status IN (%Ls))', + AlmanacDeviceStatus::getActiveStatusList()); + } else { + $where[] = qsprintf( + $conn, + '(binding.isDisabled = 1) OR (device.status IN (%Ls))', + AlmanacDeviceStatus::getDisabledStatusList()); + } + } + return $where; } + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); + + if ($this->shouldJoinDeviceTable()) { + $device_table = new AlmanacDevice(); + $joins[] = qsprintf( + $conn, + 'JOIN %R device ON binding.devicePHID = device.phid', + $device_table); + } + + return $joins; + } + + private function shouldJoinDeviceTable() { + if ($this->isActive !== null) { + return true; + } + + return false; + } + + protected function getPrimaryTableAlias() { + return 'binding'; + } + } diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php index edc55276a9..4e374ec90c 100644 --- a/src/applications/almanac/query/AlmanacServiceQuery.php +++ b/src/applications/almanac/query/AlmanacServiceQuery.php @@ -1,225 +1,248 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withServiceTypes(array $types) { $this->serviceTypes = $types; return $this; } public function withDevicePHIDs(array $phids) { $this->devicePHIDs = $phids; return $this; } public function withNamePrefix($prefix) { $this->namePrefix = $prefix; return $this; } public function withNameSuffix($suffix) { $this->nameSuffix = $suffix; return $this; } public function withNameNgrams($ngrams) { return $this->withNgramsConstraint( new AlmanacServiceNameNgrams(), $ngrams); } public function needBindings($need_bindings) { $this->needBindings = $need_bindings; return $this; } + public function needActiveBindings($need_active) { + $this->needActiveBindings = $need_active; + return $this; + } + public function newResultObject() { return new AlmanacService(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->shouldJoinBindingTable()) { $joins[] = qsprintf( $conn, 'JOIN %T binding ON service.phid = binding.servicePHID', id(new AlmanacBinding())->getTableName()); } return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'service.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'service.phid IN (%Ls)', $this->phids); } if ($this->names !== null) { $hashes = array(); foreach ($this->names as $name) { $hashes[] = PhabricatorHash::digestForIndex($name); } $where[] = qsprintf( $conn, 'service.nameIndex IN (%Ls)', $hashes); } if ($this->serviceTypes !== null) { $where[] = qsprintf( $conn, 'service.serviceType IN (%Ls)', $this->serviceTypes); } if ($this->devicePHIDs !== null) { $where[] = qsprintf( $conn, 'binding.devicePHID IN (%Ls)', $this->devicePHIDs); } if ($this->namePrefix !== null) { $where[] = qsprintf( $conn, 'service.name LIKE %>', $this->namePrefix); } if ($this->nameSuffix !== null) { $where[] = qsprintf( $conn, 'service.name LIKE %<', $this->nameSuffix); } return $where; } protected function willFilterPage(array $services) { $service_map = AlmanacServiceType::getAllServiceTypes(); foreach ($services as $key => $service) { $implementation = idx($service_map, $service->getServiceType()); if (!$implementation) { $this->didRejectResult($service); unset($services[$key]); continue; } $implementation = clone $implementation; $service->attachServiceImplementation($implementation); } return $services; } protected function didFilterPage(array $services) { - if ($this->needBindings) { + $need_all = $this->needBindings; + $need_active = $this->needActiveBindings; + + $need_any = ($need_all || $need_active); + $only_active = ($need_active && !$need_all); + + if ($need_any) { $service_phids = mpull($services, 'getPHID'); - $bindings = id(new AlmanacBindingQuery()) + + $bindings_query = id(new AlmanacBindingQuery()) ->setViewer($this->getViewer()) ->withServicePHIDs($service_phids) - ->needProperties($this->getNeedProperties()) - ->execute(); + ->needProperties($this->getNeedProperties()); + + if ($only_active) { + $bindings_query->withIsActive(true); + } + + $bindings = $bindings_query->execute(); $bindings = mgroup($bindings, 'getServicePHID'); foreach ($services as $service) { $service_bindings = idx($bindings, $service->getPHID(), array()); - $service->attachBindings($service_bindings); + + if ($only_active) { + $service->attachActiveBindings($service_bindings); + } else { + $service->attachBindings($service_bindings); + } } } return parent::didFilterPage($services); } private function shouldJoinBindingTable() { return ($this->devicePHIDs !== null); } protected function shouldGroupQueryResultRows() { if ($this->shouldJoinBindingTable()) { return true; } return parent::shouldGroupQueryResultRows(); } protected function getPrimaryTableAlias() { return 'service'; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'name', 'type' => 'string', 'unique' => true, 'reverse' => true, ), ); } protected function newPagingMapFromPartialObject($object) { return array( 'id' => (int)$object->getID(), 'name' => $object->getName(), ); } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name'), 'name' => pht('Service Name'), ), ) + parent::getBuiltinOrders(); } } diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index dfe4c27668..87704d89d8 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -1,307 +1,312 @@ setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->setStatus(AlmanacDeviceStatus::ACTIVE) ->attachAlmanacProperties(array()) ->setIsBoundToClusterService(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'nameIndex' => 'bytes12', 'status' => 'text32', 'isBoundToClusterService' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('nameIndex'), 'unique' => true, ), 'key_nametext' => array( 'columns' => array('name'), ), ), ) + parent::getConfiguration(); } public function getPHIDType() { return AlmanacDevicePHIDType::TYPECONST; } public function save() { AlmanacNames::validateName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); return parent::save(); } public function getURI() { return urisprintf( '/almanac/device/view/%s/', $this->getName()); } public function rebuildClusterBindingStatus() { $services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDevicePHIDs(array($this->getPHID())) ->execute(); $is_cluster = false; foreach ($services as $service) { if ($service->isClusterService()) { $is_cluster = true; break; } } if ($is_cluster != $this->getIsBoundToClusterService()) { $this->setIsBoundToClusterService((int)$is_cluster); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), 'UPDATE %R SET isBoundToClusterService = %d WHERE id = %d', $this, $this->getIsBoundToClusterService(), $this->getID()); unset($unguarded); } return $this; } public function isClusterDevice() { return $this->getIsBoundToClusterService(); } public function getStatusObject() { return $this->newStatusObject(); } private function newStatusObject() { return AlmanacDeviceStatus::newStatusFromValue($this->getStatus()); } public function isDisabled() { return $this->getStatusObject()->isDisabled(); } /* -( AlmanacPropertyInterface )------------------------------------------- */ public function attachAlmanacProperties(array $properties) { assert_instances_of($properties, 'AlmanacProperty'); $this->almanacProperties = mpull($properties, null, 'getFieldName'); return $this; } public function getAlmanacProperties() { return $this->assertAttached($this->almanacProperties); } public function hasAlmanacProperty($key) { $this->assertAttached($this->almanacProperties); return isset($this->almanacProperties[$key]); } public function getAlmanacProperty($key) { return $this->assertAttachedKey($this->almanacProperties, $key); } public function getAlmanacPropertyValue($key, $default = null) { if ($this->hasAlmanacProperty($key)) { return $this->getAlmanacProperty($key)->getFieldValue(); } else { return $default; } } public function getAlmanacPropertyFieldSpecifications() { return array(); } public function newAlmanacPropertyEditEngine() { return new AlmanacDevicePropertyEditEngine(); } public function getAlmanacPropertySetTransactionType() { return AlmanacDeviceSetPropertyTransaction::TRANSACTIONTYPE; } public function getAlmanacPropertyDeleteTransactionType() { return AlmanacDeviceDeletePropertyTransaction::TRANSACTIONTYPE; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->isClusterDevice()) { return array( array( new PhabricatorAlmanacApplication(), AlmanacManageClusterServicesCapability::CAPABILITY, ), ); } break; } return array(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacDeviceEditor(); } public function getApplicationTransactionTemplate() { return new AlmanacDeviceTransaction(); } /* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */ public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) { return $this->getURI(); } public function getSSHKeyDefaultName() { return $this->getName(); } public function getSSHKeyNotifyPHIDs() { // Devices don't currently have anyone useful to notify about SSH key // edits, and they're usually a difficult vector to attack since you need // access to a cluster host. However, it would be nice to make them // subscribable at some point. return array(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $interfaces = id(new AlmanacInterfaceQuery()) ->setViewer($engine->getViewer()) ->withDevicePHIDs(array($this->getPHID())) ->execute(); foreach ($interfaces as $interface) { $engine->destroyObject($interface); } $this->delete(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new AlmanacDeviceNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the device.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('Device status information.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('disabled') + ->setType('bool') + ->setDescription(pht('True if device is disabled.')), ); } public function getFieldValuesForConduit() { $status = $this->getStatusObject(); return array( 'name' => $this->getName(), 'status' => array( 'value' => $status->getValue(), 'name' => $status->getName(), ), + 'disabled' => $this->isDisabled(), ); } public function getConduitSearchAttachments() { return array( id(new AlmanacPropertiesSearchEngineAttachment()) ->setAttachmentKey('properties'), ); } } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index 2979b74367..1683ba1340 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -1,295 +1,312 @@ setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->attachAlmanacProperties(array()) ->setServiceType($type) ->attachServiceImplementation($implementation); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', 'serviceType' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('nameIndex'), 'unique' => true, ), 'key_nametext' => array( 'columns' => array('name'), ), 'key_servicetype' => array( 'columns' => array('serviceType'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(AlmanacServicePHIDType::TYPECONST); } public function save() { AlmanacNames::validateName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } public function getURI() { return '/almanac/service/view/'.$this->getName().'/'; } public function getBindings() { return $this->assertAttached($this->bindings); } public function getActiveBindings() { - $bindings = $this->getBindings(); + return $this->assertAttached($this->activeBindings); + } - // Filter out disabled bindings. + public function attachBindings(array $bindings) { + $active_bindings = array(); foreach ($bindings as $key => $binding) { + // Filter out disabled bindings. if ($binding->getIsDisabled()) { - unset($bindings[$key]); + continue; + } + + // Filter out bindings to disabled devices. + if ($binding->getDevice()->isDisabled()) { + continue; } + + $active_bindings[$key] = $binding; } - return $bindings; - } + $this->attachActiveBindings($active_bindings); - public function attachBindings(array $bindings) { $this->bindings = $bindings; return $this; } + public function attachActiveBindings(array $bindings) { + $this->activeBindings = $bindings; + return $this; + } + public function getServiceImplementation() { return $this->assertAttached($this->serviceImplementation); } public function attachServiceImplementation(AlmanacServiceType $type) { $this->serviceImplementation = $type; return $this; } public function isClusterService() { return $this->getServiceImplementation()->isClusterServiceType(); } /* -( AlmanacPropertyInterface )------------------------------------------- */ public function attachAlmanacProperties(array $properties) { assert_instances_of($properties, 'AlmanacProperty'); $this->almanacProperties = mpull($properties, null, 'getFieldName'); return $this; } public function getAlmanacProperties() { return $this->assertAttached($this->almanacProperties); } public function hasAlmanacProperty($key) { $this->assertAttached($this->almanacProperties); return isset($this->almanacProperties[$key]); } public function getAlmanacProperty($key) { return $this->assertAttachedKey($this->almanacProperties, $key); } public function getAlmanacPropertyValue($key, $default = null) { if ($this->hasAlmanacProperty($key)) { return $this->getAlmanacProperty($key)->getFieldValue(); } else { return $default; } } public function getAlmanacPropertyFieldSpecifications() { return $this->getServiceImplementation()->getFieldSpecifications(); } public function getBindingFieldSpecifications(AlmanacBinding $binding) { $impl = $this->getServiceImplementation(); return $impl->getBindingFieldSpecifications($binding); } public function newAlmanacPropertyEditEngine() { return new AlmanacServicePropertyEditEngine(); } public function getAlmanacPropertySetTransactionType() { return AlmanacServiceSetPropertyTransaction::TRANSACTIONTYPE; } public function getAlmanacPropertyDeleteTransactionType() { return AlmanacServiceDeletePropertyTransaction::TRANSACTIONTYPE; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->isClusterService()) { return array( array( new PhabricatorAlmanacApplication(), AlmanacManageClusterServicesCapability::CAPABILITY, ), ); } break; } return array(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacServiceEditor(); } public function getApplicationTransactionTemplate() { return new AlmanacServiceTransaction(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $bindings = id(new AlmanacBindingQuery()) ->setViewer($engine->getViewer()) ->withServicePHIDs(array($this->getPHID())) ->execute(); foreach ($bindings as $binding) { $engine->destroyObject($binding); } $this->delete(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new AlmanacServiceNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the service.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('serviceType') ->setType('string') ->setDescription(pht('The service type constant.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'serviceType' => $this->getServiceType(), ); } public function getConduitSearchAttachments() { return array( id(new AlmanacPropertiesSearchEngineAttachment()) ->setAttachmentKey('properties'), id(new AlmanacBindingsSearchEngineAttachment()) ->setAttachmentKey('bindings'), + id(new AlmanacBindingsSearchEngineAttachment()) + ->setIsActive(true) + ->setAttachmentKey('activeBindings'), ); } } diff --git a/src/applications/almanac/view/AlmanacBindingTableView.php b/src/applications/almanac/view/AlmanacBindingTableView.php index 746d3de5c2..a74cb4f9c3 100644 --- a/src/applications/almanac/view/AlmanacBindingTableView.php +++ b/src/applications/almanac/view/AlmanacBindingTableView.php @@ -1,119 +1,139 @@ noDataString = $no_data_string; return $this; } public function getNoDataString() { return $this->noDataString; } public function setBindings(array $bindings) { $this->bindings = $bindings; return $this; } public function getBindings() { return $this->bindings; } public function setHideServiceColumn($hide_service_column) { $this->hideServiceColumn = $hide_service_column; return $this; } public function getHideServiceColumn() { return $this->hideServiceColumn; } public function render() { $bindings = $this->getBindings(); $viewer = $this->getUser(); $phids = array(); foreach ($bindings as $binding) { $phids[] = $binding->getServicePHID(); $phids[] = $binding->getDevicePHID(); $phids[] = $binding->getInterface()->getNetworkPHID(); } $handles = $viewer->loadHandles($phids); $icon_disabled = id(new PHUIIconView()) ->setIcon('fa-ban') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Disabled'), )); $icon_active = id(new PHUIIconView()) ->setIcon('fa-check') + ->setColor('green') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Active'), )); + $icon_device_disabled = id(new PHUIIconView()) + ->setIcon('fa-times') + ->setColor('grey') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Device Disabled'), + )); + $rows = array(); foreach ($bindings as $binding) { $addr = $binding->getInterface()->getAddress(); $port = $binding->getInterface()->getPort(); + $device = $binding->getDevice(); + if ($device->isDisabled()) { + $binding_icon = $icon_device_disabled; + } else if ($binding->getIsDisabled()) { + $binding_icon = $icon_disabled; + } else { + $binding_icon = $icon_active; + } + $rows[] = array( $binding->getID(), - ($binding->getIsDisabled() ? $icon_disabled : $icon_active), + $binding_icon, $handles->renderHandle($binding->getServicePHID()), + $handles->renderHandle($binding->getDevicePHID()), $handles->renderHandle($binding->getInterface()->getNetworkPHID()), $binding->getInterface()->renderDisplayAddress(), phutil_tag( 'a', array( 'class' => 'small button button-grey', 'href' => '/almanac/binding/'.$binding->getID().'/', ), pht('Details')), ); } $table = id(new AphrontTableView($rows)) ->setNoDataString($this->getNoDataString()) ->setHeaders( array( pht('ID'), null, pht('Service'), pht('Device'), pht('Network'), pht('Interface'), null, )) ->setColumnClasses( array( '', 'icon', '', '', '', 'wide', 'action', )) ->setColumnVisibility( array( true, true, !$this->getHideServiceColumn(), )); return $table; } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php index 86ff93816c..f657a81641 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php @@ -1,392 +1,369 @@ getRepository(); if ($repository->getAlmanacServicePHID()) { return 'fa-sitemap'; } else if ($repository->isHosted()) { return 'fa-database'; } else { return 'fa-download'; } } public function buildManagementPanelCurtain() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $action_list = $this->newActionList(); $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories'); $action_list->addAction( id(new PhabricatorActionView()) ->setIcon('fa-book') ->setHref($doc_href) ->setName(pht('Cluster Documentation'))); return $this->newCurtainView() ->setActionList($action_list); } public function buildManagementPanelContent() { return array( $this->buildStorageStatusPanel(), $this->buildClusterStatusPanel(), $this->buildRefsStatusPanels(), ); } private function buildStorageStatusPanel() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer); if ($repository->usesLocalWorkingCopy()) { $storage_path = $repository->getLocalPath(); } else { $storage_path = phutil_tag('em', array(), pht('No Local Working Copy')); } $service_phid = $repository->getAlmanacServicePHID(); if ($service_phid) { $storage_service = $viewer->renderHandle($service_phid); } else { $storage_service = phutil_tag('em', array(), pht('Local')); } $view->addProperty(pht('Storage Path'), $storage_path); $view->addProperty(pht('Storage Cluster'), $storage_service); return $this->newBox(pht('Storage'), $view); } private function buildClusterStatusPanel() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $service_phid = $repository->getAlmanacServicePHID(); if ($service_phid) { $service = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withServiceTypes( array( AlmanacClusterRepositoryServiceType::SERVICETYPE, )) ->withPHIDs(array($service_phid)) - ->needBindings(true) + ->needActiveBindings(true) ->executeOne(); if (!$service) { // TODO: Viewer may not have permission to see the service, or it may // be invalid? Raise some more useful error here? throw new Exception(pht('Unable to load cluster service.')); } } else { $service = null; } Javelin::initBehavior('phabricator-tooltips'); $rows = array(); if ($service) { - $bindings = $service->getBindings(); + $bindings = $service->getActiveBindings(); $bindings = mgroup($bindings, 'getDevicePHID'); // This is an unusual read which always comes from the master. if (PhabricatorEnv::isReadOnly()) { $versions = array(); } else { $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( $repository->getPHID()); } $versions = mpull($versions, null, 'getDevicePHID'); - // List enabled devices first, then sort devices in each group by name. $sort = array(); foreach ($bindings as $key => $binding_group) { - $all_disabled = $this->isDisabledGroup($binding_group); - $sort[$key] = id(new PhutilSortVector()) - ->addInt($all_disabled ? 1 : 0) ->addString(head($binding_group)->getDevice()->getName()); } $sort = msortv($sort, 'getSelf'); $bindings = array_select_keys($bindings, array_keys($sort)) + $bindings; foreach ($bindings as $binding_group) { - $all_disabled = $this->isDisabledGroup($binding_group); $any_binding = head($binding_group); - if ($all_disabled) { - $binding_icon = 'fa-times grey'; - $binding_tip = pht('Disabled'); - } else { - $binding_icon = 'fa-folder-open green'; - $binding_tip = pht('Active'); - } + $binding_icon = 'fa-folder-open green'; + $binding_tip = pht('Active'); $binding_icon = id(new PHUIIconView()) ->setIcon($binding_icon) ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => $binding_tip, )); $device = $any_binding->getDevice(); $version = idx($versions, $device->getPHID()); if ($version) { $version_number = $version->getRepositoryVersion(); $href = null; if ($repository->isHosted()) { $href = "/diffusion/pushlog/view/{$version_number}/"; } else { $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIDs(array($version_number)) ->executeOne(); if ($commit) { $href = $commit->getURI(); } } if ($href) { $version_number = phutil_tag( 'a', array( 'href' => $href, ), $version_number); } } else { $version_number = '-'; } if ($version && $version->getIsWriting()) { $is_writing = id(new PHUIIconView()) ->setIcon('fa-pencil green'); } else { $is_writing = id(new PHUIIconView()) ->setIcon('fa-pencil grey'); } $write_properties = null; if ($version) { $write_properties = $version->getWriteProperties(); if ($write_properties) { try { $write_properties = phutil_json_decode($write_properties); } catch (Exception $ex) { $write_properties = null; } } } if ($write_properties) { $writer_phid = idx($write_properties, 'userPHID'); $last_writer = $viewer->renderHandle($writer_phid); $writer_epoch = idx($write_properties, 'epoch'); $writer_epoch = phabricator_datetime($writer_epoch, $viewer); } else { $last_writer = null; $writer_epoch = null; } $rows[] = array( $binding_icon, phutil_tag( 'a', array( 'href' => $device->getURI(), ), $device->getName()), $version_number, $is_writing, $last_writer, $writer_epoch, ); } } $table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This is not a cluster repository.')) ->setHeaders( array( null, pht('Device'), pht('Version'), pht('Writing'), pht('Last Writer'), pht('Last Write At'), )) ->setColumnClasses( array( null, null, null, 'right wide', null, 'date', )); return $this->newBox(pht('Cluster Status'), $table); } private function buildRefsStatusPanels() { $repository = $this->getRepository(); $service_phid = $repository->getAlmanacServicePHID(); if (!$service_phid) { // If this repository isn't clustered, don't bother rendering anything. // There are enough other context clues that another empty panel isn't // useful. return; } $all_protocols = array( 'http', 'https', 'ssh', ); $readable_panel = $this->buildRefsStatusPanel( pht('Readable Service Refs'), array( 'neverProxy' => false, 'protocols' => $all_protocols, 'writable' => false, )); $writable_panel = $this->buildRefsStatusPanel( pht('Writable Service Refs'), array( 'neverProxy' => false, 'protocols' => $all_protocols, 'writable' => true, )); return array( $readable_panel, $writable_panel, ); } private function buildRefsStatusPanel( $title, $options) { $repository = $this->getRepository(); $viewer = $this->getViewer(); $caught = null; try { $refs = $repository->getAlmanacServiceRefs($viewer, $options); } catch (Exception $ex) { $caught = $ex; } catch (Throwable $ex) { $caught = $ex; } $info_view = null; if ($caught) { $refs = array(); $info_view = id(new PHUIInfoView()) ->setErrors( array( phutil_escape_html_newlines($caught->getMessage()), )); } $phids = array(); foreach ($refs as $ref) { $phids[] = $ref->getDevicePHID(); } $handles = $viewer->loadHandles($phids); $icon_writable = id(new PHUIIconView()) ->setIcon('fa-pencil', 'green'); $icon_unwritable = id(new PHUIIconView()) ->setIcon('fa-times', 'grey'); $rows = array(); foreach ($refs as $ref) { $device_phid = $ref->getDevicePHID(); $device_handle = $handles[$device_phid]; if ($ref->isWritable()) { $writable_icon = $icon_writable; $writable_text = pht('Read/Write'); } else { $writable_icon = $icon_unwritable; $writable_text = pht('Read Only'); } $rows[] = array( $device_handle->renderLink(), $ref->getURI(), $writable_icon, $writable_text, ); } $table = id(new AphrontTableView($rows)) ->setNoDataString(pht('No repository service refs available.')) ->setHeaders( array( pht('Device'), pht('Internal Service URI'), null, pht('I/O'), )) ->setColumnClasses( array( null, 'wide', 'icon', null, )); $box_view = $this->newBox($title, $table); if ($info_view) { $box_view->setInfoView($info_view); } return $box_view; } - - private function isDisabledGroup(array $binding_group) { - assert_instances_of($binding_group, 'AlmanacBinding'); - - foreach ($binding_group as $binding) { - if (!$binding->getIsDisabled()) { - return false; - } - } - - return true; - } - } diff --git a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php index 12ccf8a343..6c8e75696f 100644 --- a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php @@ -1,327 +1,322 @@ loadServices($blueprint); - $bindings = $this->loadAllBindings($services); + $bindings = $this->getActiveBindings($services); if (!$bindings) { // If there are no devices bound to the services for this blueprint, // we can not allocate resources. return false; } return true; } public function shouldAllocateSupplementalResource( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // We want to use every host in an Almanac service, since the amount of // hardware is fixed and there's normally no value in packing leases onto a // subset of it. Always build a new supplemental resource if we can. return true; } public function canAllocateResourceForLease( DrydockBlueprint $blueprint, DrydockLease $lease) { // We will only allocate one resource per unique device bound to the // services for this blueprint. Make sure we have a free device somewhere. $free_bindings = $this->loadFreeBindings($blueprint); if (!$free_bindings) { return false; } return true; } public function allocateResource( DrydockBlueprint $blueprint, DrydockLease $lease) { $free_bindings = $this->loadFreeBindings($blueprint); shuffle($free_bindings); $exceptions = array(); foreach ($free_bindings as $binding) { $device = $binding->getDevice(); $device_name = $device->getName(); $binding_phid = $binding->getPHID(); $resource = $this->newResourceTemplate($blueprint) ->setActivateWhenAllocated(true) ->setAttribute('almanacDeviceName', $device_name) ->setAttribute('almanacServicePHID', $binding->getServicePHID()) ->setAttribute('almanacBindingPHID', $binding_phid) ->needSlotLock("almanac.host.binding({$binding_phid})"); try { return $resource->allocateResource(); } catch (Exception $ex) { $exceptions[] = $ex; } } throw new PhutilAggregateException( pht('Unable to allocate any binding as a resource.'), $exceptions); } public function destroyResource( DrydockBlueprint $blueprint, DrydockResource $resource) { // We don't create anything when allocating hosts, so we don't need to do // any cleanup here. return; } public function getResourceName( DrydockBlueprint $blueprint, DrydockResource $resource) { $device_name = $resource->getAttribute( 'almanacDeviceName', pht('')); return pht('Host (%s)', $device_name); } public function canAcquireLeaseOnResource( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // Require the binding to a given host be active before we'll hand out more // leases on the corresponding resource. $binding = $this->loadBindingForResource($resource); if ($binding->getIsDisabled()) { return false; } return true; } public function acquireLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { $lease ->setActivateWhenAcquired(true) ->acquireOnResource($resource); } public function didReleaseLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // Almanac hosts stick around indefinitely so we don't need to recycle them // if they don't have any leases. return; } public function destroyLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // We don't create anything when activating a lease, so we don't need to // throw anything away. return; } public function getType() { return 'host'; } public function getInterface( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease, $type) { switch ($type) { case DrydockCommandInterface::INTERFACE_TYPE: $credential_phid = $blueprint->getFieldValue('credentialPHID'); $binding = $this->loadBindingForResource($resource); $interface = $binding->getInterface(); return id(new DrydockSSHCommandInterface()) ->setConfig('credentialPHID', $credential_phid) ->setConfig('host', $interface->getAddress()) ->setConfig('port', $interface->getPort()); } } protected function getCustomFieldSpecifications() { return array( 'almanacServicePHIDs' => array( 'name' => pht('Almanac Services'), 'type' => 'datasource', 'datasource.class' => 'AlmanacServiceDatasource', 'datasource.parameters' => array( 'serviceTypes' => $this->getAlmanacServiceTypes(), ), 'required' => true, ), 'credentialPHID' => array( 'name' => pht('Credentials'), 'type' => 'credential', 'credential.provides' => PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE, 'credential.type' => PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE, ), ); } private function loadServices(DrydockBlueprint $blueprint) { if (!$this->services) { $service_phids = $blueprint->getFieldValue('almanacServicePHIDs'); if (!$service_phids) { throw new Exception( pht( 'This blueprint ("%s") does not define any Almanac Service PHIDs.', $blueprint->getBlueprintName())); } $viewer = $this->getViewer(); $services = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withPHIDs($service_phids) ->withServiceTypes($this->getAlmanacServiceTypes()) - ->needBindings(true) + ->needActiveBindings(true) ->execute(); $services = mpull($services, null, 'getPHID'); if (count($services) != count($service_phids)) { $missing_phids = array_diff($service_phids, array_keys($services)); throw new Exception( pht( 'Some of the Almanac Services defined by this blueprint '. 'could not be loaded. They may be invalid, no longer exist, '. 'or be of the wrong type: %s.', implode(', ', $missing_phids))); } $this->services = $services; } return $this->services; } - private function loadAllBindings(array $services) { + private function getActive(array $services) { assert_instances_of($services, 'AlmanacService'); - $bindings = array_mergev(mpull($services, 'getBindings')); + $bindings = array_mergev(mpull($services, 'getActiveBindings')); return mpull($bindings, null, 'getPHID'); } private function loadFreeBindings(DrydockBlueprint $blueprint) { if ($this->freeBindings === null) { $viewer = $this->getViewer(); $pool = id(new DrydockResourceQuery()) ->setViewer($viewer) ->withBlueprintPHIDs(array($blueprint->getPHID())) ->withStatuses( array( DrydockResourceStatus::STATUS_PENDING, DrydockResourceStatus::STATUS_ACTIVE, DrydockResourceStatus::STATUS_BROKEN, DrydockResourceStatus::STATUS_RELEASED, )) ->execute(); $allocated_phids = array(); foreach ($pool as $resource) { $allocated_phids[] = $resource->getAttribute('almanacBindingPHID'); } $allocated_phids = array_fuse($allocated_phids); $services = $this->loadServices($blueprint); - $bindings = $this->loadAllBindings($services); + $bindings = $this->getActiveBindings($services); $free = array(); foreach ($bindings as $binding) { - // Don't consider disabled bindings to be available. - if ($binding->getIsDisabled()) { - continue; - } - if (empty($allocated_phids[$binding->getPHID()])) { $free[] = $binding; } } $this->freeBindings = $free; } return $this->freeBindings; } private function getAlmanacServiceTypes() { return array( AlmanacDrydockPoolServiceType::SERVICETYPE, ); } private function loadBindingForResource(DrydockResource $resource) { $binding_phid = $resource->getAttribute('almanacBindingPHID'); if (!$binding_phid) { throw new Exception( pht( 'Drydock resource ("%s") has no Almanac binding PHID, so its '. 'binding can not be loaded.', $resource->getPHID())); } $viewer = $this->getViewer(); $binding = id(new AlmanacBindingQuery()) ->setViewer($viewer) ->withPHIDs(array($binding_phid)) ->executeOne(); if (!$binding) { throw new Exception( pht( 'Unable to load Almanac binding ("%s") for resource ("%s").', $binding_phid, $resource->getPHID())); } return $binding; } } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php index 2e2c3fcc92..c3de833a1f 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php @@ -1,151 +1,151 @@ setName('clusterize') ->setExamples('**clusterize** [options] __repository__ ...') ->setSynopsis( pht('Convert existing repositories into cluster repositories.')) ->setArguments( array( array( 'name' => 'service', 'param' => 'service', 'help' => pht( 'Cluster repository service in Almanac to move repositories '. 'into.'), ), array( 'name' => 'remove-service', 'help' => pht('Take repositories out of a cluster.'), ), array( 'name' => 'repositories', 'wildcard' => true, ), )); } public function execute(PhutilArgumentParser $args) { $viewer = $this->getViewer(); $repositories = $this->loadRepositories($args, 'repositories'); if (!$repositories) { throw new PhutilArgumentUsageException( pht('Specify one or more repositories to clusterize.')); } $service_name = $args->getArg('service'); $remove_service = $args->getArg('remove-service'); if ($remove_service && $service_name) { throw new PhutilArgumentUsageException( pht('Specify --service or --remove-service, but not both.')); } if (!$service_name && !$remove_service) { throw new PhutilArgumentUsageException( pht('Specify --service or --remove-service.')); } if ($remove_service) { $service = null; } else { $service = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withNames(array($service_name)) ->withServiceTypes( array( AlmanacClusterRepositoryServiceType::SERVICETYPE, )) - ->needBindings(true) + ->needActiveBindings(true) ->executeOne(); if (!$service) { throw new PhutilArgumentUsageException( pht( 'No repository service "%s" exists.', $service_name)); } } if ($service) { $service_phid = $service->getPHID(); $bindings = $service->getActiveBindings(); $unique_devices = array(); foreach ($bindings as $binding) { $unique_devices[$binding->getDevicePHID()] = $binding->getDevice(); } if (count($unique_devices) > 1) { $device_names = mpull($unique_devices, 'getName'); echo id(new PhutilConsoleBlock()) ->addParagraph( pht( 'Service "%s" is actively bound to more than one device (%s).', $service_name, implode(', ', $device_names))) ->addParagraph( pht( 'If you clusterize a repository onto this service it may be '. 'unclear which devices have up-to-date copies of the '. 'repository. If so, leader/follower ambiguity will freeze the '. 'repository. You may need to manually promote a device to '. 'unfreeze it. See "Ambiguous Leaders" in the documentation '. 'for discussion.')) ->drawConsoleString(); $prompt = pht('Continue anyway?'); if (!phutil_console_confirm($prompt)) { throw new PhutilArgumentUsageException( pht('User aborted the workflow.')); } } } else { $service_phid = null; } $content_source = $this->newContentSource(); $diffusion_phid = id(new PhabricatorDiffusionApplication())->getPHID(); foreach ($repositories as $repository) { $xactions = array(); $xactions[] = id(new PhabricatorRepositoryTransaction()) ->setTransactionType( PhabricatorRepositoryServiceTransaction::TRANSACTIONTYPE) ->setNewValue($service_phid); id(new PhabricatorRepositoryEditor()) ->setActor($viewer) ->setActingAsPHID($diffusion_phid) ->setContentSource($content_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($repository, $xactions); if ($service) { echo tsprintf( "%s\n", pht( 'Moved repository "%s" to cluster service "%s".', $repository->getDisplayName(), $service->getName())); } else { echo tsprintf( "%s\n", pht( 'Removed repository "%s" from cluster service.', $repository->getDisplayName())); } } return 0; } } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 508a886185..8c98dd8756 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1,2881 +1,2881 @@ 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/?(?:diffusion|source)/(?P[^/]+))'. '(?P.*)'. '\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/(?:diffusion|source)/)'. '(?P[^/]+)'. '(?P(?:/.*)?)'. '\z)'; $matches = null; if (preg_match($standard_pattern, $request_path, $matches)) { $suffix = $matches['suffix']; return $this->getPathURI($suffix); } $commit_pattern = '(^'. '(?P/)'. '(?P'. '(?:'. 'r(?P[A-Z]+)'. '|'. 'R(?P[1-9]\d*):'. ')'. '(?P[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 '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 '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@arcanist: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 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); if (!$refs) { return null; } $ref = head($refs); return $ref->getURI(); } public function getAlmanacServiceRefs( PhabricatorUser $viewer, array $options) { PhutilTypeSpec::checkMap( $options, array( 'neverProxy' => 'bool', 'protocols' => 'list', 'writable' => 'optional bool', )); $never_proxy = $options['neverProxy']; $protocols = $options['protocols']; $writable = idx($options, 'writable', false); $cache_key = $this->getAlmanacServiceCacheKey(); if (!$cache_key) { return array(); } $cache = PhabricatorCaches::getMutableStructureCache(); $uris = $cache->getKey($cache_key, false); // If we haven't built the cache yet, build it now. if ($uris === false) { $uris = $this->buildAlmanacServiceURIs(); $cache->setKey($cache_key, $uris); } if ($uris === null) { return array(); } $local_device = AlmanacKeys::getDeviceID(); if ($never_proxy && !$local_device) { throw new Exception( pht( 'Unable to handle proxied service request. This device is not '. 'registered, so it can not identify local services. Register '. 'this device before sending requests here.')); } $protocol_map = array_fuse($protocols); $results = array(); foreach ($uris as $uri) { // If we're never proxying this and it's locally satisfiable, return // `null` to tell the caller to handle it locally. If we're allowed to // proxy, we skip this check and may proxy the request to ourselves. // (That proxied request will end up here with proxying forbidden, // return `null`, and then the request will actually run.) if ($local_device && $never_proxy) { if ($uri['device'] == $local_device) { return array(); } } if (isset($protocol_map[$uri['protocol']])) { $results[] = $uri; } } if (!$results) { throw new Exception( pht( 'The Almanac service for this repository is not bound to any '. 'interfaces which support the required protocols (%s).', implode(', ', $protocols))); } if ($never_proxy) { // See PHI1030. This error can arise from various device name/address // mismatches which are hard to detect, so try to provide as much // information as we can. if ($writable) { $request_type = pht('(This is a write request.)'); } else { $request_type = pht('(This is a read request.)'); } throw new Exception( pht( 'This repository request (for repository "%s") has been '. 'incorrectly routed to a cluster host (with device name "%s", '. 'and hostname "%s") which can not serve the request.'. "\n\n". 'The Almanac device address for the correct device may improperly '. 'point at this host, or the "device.id" configuration file on '. 'this host may be incorrect.'. "\n\n". 'Requests routed within the cluster by Phabricator are always '. 'expected to be sent to a node which can serve the request. To '. 'prevent loops, this request will not be proxied again.'. "\n\n". "%s", $this->getDisplayName(), $local_device, php_uname('n'), $request_type)); } if (count($results) > 1) { if (!$this->supportsSynchronization()) { throw new Exception( pht( 'Repository "%s" is bound to multiple active repository hosts, '. 'but this repository does not support cluster synchronization. '. 'Declusterize this repository or move it to a service with only '. 'one host.', $this->getDisplayName())); } } $refs = array(); foreach ($results as $result) { $refs[] = DiffusionServiceRef::newFromDictionary($result); } // If we require a writable device, remove URIs which aren't writable. if ($writable) { foreach ($refs as $key => $ref) { if (!$ref->isWritable()) { unset($refs[$key]); } } if (!$refs) { throw new Exception( pht( 'This repository ("%s") is not writable with the given '. 'protocols (%s). The Almanac service for this repository has no '. 'writable bindings that support these protocols.', $this->getDisplayName(), implode(', ', $protocols))); } } if ($writable) { $refs = $this->sortWritableAlmanacServiceRefs($refs); } else { $refs = $this->sortReadableAlmanacServiceRefs($refs); } return array_values($refs); } private function sortReadableAlmanacServiceRefs(array $refs) { assert_instances_of($refs, 'DiffusionServiceRef'); shuffle($refs); return $refs; } private function sortWritableAlmanacServiceRefs(array $refs) { assert_instances_of($refs, 'DiffusionServiceRef'); // See T13109 for discussion of how this method routes requests. // In the absence of other rules, we'll send traffic to devices randomly. // We also want to select randomly among nodes which are equally good // candidates to receive the write, and accomplish that by shuffling the // list up front. shuffle($refs); $order = array(); // If some device is currently holding the write lock, send all requests // to that device. We're trying to queue writes on a single device so they // do not need to wait for read synchronization after earlier writes // complete. $writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter( $this->getPHID()); if ($writer) { $device_phid = $writer->getWriteProperty('devicePHID'); foreach ($refs as $key => $ref) { if ($ref->getDevicePHID() === $device_phid) { $order[] = $key; } } } // If no device is currently holding the write lock, try to send requests // to a device which is already up to date and will not need to synchronize // before it can accept the write. $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( $this->getPHID()); if ($versions) { $max_version = (int)max(mpull($versions, 'getRepositoryVersion')); $max_devices = array(); foreach ($versions as $version) { if ($version->getRepositoryVersion() == $max_version) { $max_devices[] = $version->getDevicePHID(); } } $max_devices = array_fuse($max_devices); foreach ($refs as $key => $ref) { if (isset($max_devices[$ref->getDevicePHID()])) { $order[] = $key; } } } // Reorder the results, putting any we've selected as preferred targets for // the write at the head of the list. $refs = array_select_keys($refs, $order) + $refs; return $refs; } public function supportsSynchronization() { // TODO: For now, this is only supported for Git. if (!$this->isGit()) { return false; } return true; } public function supportsRefs() { if ($this->isSVN()) { return false; } return true; } public function getAlmanacServiceCacheKey() { $service_phid = $this->getAlmanacServicePHID(); if (!$service_phid) { return null; } $repository_phid = $this->getPHID(); $parts = array( "repo({$repository_phid})", "serv({$service_phid})", 'v4', ); return implode('.', $parts); } private function buildAlmanacServiceURIs() { $service = $this->loadAlmanacService(); if (!$service) { return null; } $bindings = $service->getActiveBindings(); if (!$bindings) { throw new Exception( pht( 'The Almanac service for this repository is not bound to any '. - 'interfaces.')); + 'active interfaces.')); } $uris = array(); foreach ($bindings as $binding) { $iface = $binding->getInterface(); $uri = $this->getClusterRepositoryURIFromBinding($binding); $protocol = $uri->getProtocol(); $device_name = $iface->getDevice()->getName(); $device_phid = $iface->getDevice()->getPHID(); $uris[] = array( 'protocol' => $protocol, 'uri' => (string)$uri, 'device' => $device_name, 'writable' => (bool)$binding->getAlmanacPropertyValue('writable'), 'devicePHID' => $device_phid, ); } return $uris; } /** * Build a new Conduit client in order to make a service call to this * repository. * * If the repository is hosted locally, this method may return `null`. The * caller should use `ConduitCall` or other local logic to complete the * request. * * By default, we will return a @{class:ConduitClient} for any repository with * a service, even if that service is on the current device. * * We do this because this configuration does not make very much sense in a * production context, but is very common in a test/development context * (where the developer's machine is both the web host and the repository * service). By proxying in development, we get more consistent behavior * between development and production, and don't have a major untested * codepath. * * The `$never_proxy` parameter can be used to prevent this local proxying. * If the flag is passed: * * - The method will return `null` (implying a local service call) * if the repository service is hosted on the current device. * - The method will throw if it would need to return a client. * * This is used to prevent loops in Conduit: the first request will proxy, * even in development, but the second request will be identified as a * cluster request and forced not to proxy. * * For lower-level service resolution, see @{method:getAlmanacServiceURI}. * * @param PhabricatorUser Viewing user. * @param bool `true` to throw if a client would be returned. * @return ConduitClient|null Client, or `null` for local repositories. */ public function newConduitClient( PhabricatorUser $viewer, $never_proxy = false) { $uri = $this->getAlmanacServiceURI( $viewer, array( 'neverProxy' => $never_proxy, 'protocols' => array( 'http', 'https', ), // At least today, no Conduit call can ever write to a repository, // so it's fine to send anything to a read-only node. 'writable' => false, )); if ($uri === null) { return null; } $domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain(); $client = id(new ConduitClient($uri)) ->setHost($domain); if ($viewer->isOmnipotent()) { // If the caller is the omnipotent user (normally, a daemon), we will // sign the request with this host's asymmetric keypair. $public_path = AlmanacKeys::getKeyPath('device.pub'); try { $public_key = Filesystem::readFile($public_path); } catch (Exception $ex) { throw new PhutilAggregateException( pht( 'Unable to read device public key while attempting to make '. 'authenticated method call within the Phabricator cluster. '. 'Use `%s` to register keys for this device. Exception: %s', 'bin/almanac register', $ex->getMessage()), array($ex)); } $private_path = AlmanacKeys::getKeyPath('device.key'); try { $private_key = Filesystem::readFile($private_path); $private_key = new PhutilOpaqueEnvelope($private_key); } catch (Exception $ex) { throw new PhutilAggregateException( pht( 'Unable to read device private key while attempting to make '. 'authenticated method call within the Phabricator cluster. '. 'Use `%s` to register keys for this device. Exception: %s', 'bin/almanac register', $ex->getMessage()), array($ex)); } $client->setSigningKeys($public_key, $private_key); } else { // If the caller is a normal user, we generate or retrieve a cluster // API token. $token = PhabricatorConduitToken::loadClusterTokenForUser($viewer); if ($token) { $client->setConduitToken($token->getToken()); } } return $client; } public function newConduitClientForRequest(ConduitAPIRequest $request) { // Figure out whether we're going to handle this request on this device, // or proxy it to another node in the cluster. // If this is a cluster request and we need to proxy, we'll explode here // to prevent infinite recursion. $viewer = $request->getViewer(); $is_cluster_request = $request->getIsClusterRequest(); $client = $this->newConduitClient( $viewer, $is_cluster_request); return $client; } public function newConduitFuture( PhabricatorUser $viewer, $method, array $params, $never_proxy = false) { $client = $this->newConduitClient( $viewer, $never_proxy); if (!$client) { $result = id(new ConduitCall($method, $params)) ->setUser($viewer) ->execute(); $future = new ImmediateFuture($result); } else { $future = $client->callMethod($method, $params); } return $future; } public function getPassthroughEnvironmentalVariables() { $env = $_ENV; if ($this->isGit()) { // $_ENV does not populate in CLI contexts if "E" is missing from // "variables_order" in PHP config. Currently, we do not require this // to be configured. Since it may not be, explicitly bring expected Git // environmental variables into scope. This list is not exhaustive, but // only lists variables with a known impact on commit hook behavior. // This can be removed if we later require "E" in "variables_order". $git_env = array( 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_QUARANTINE_PATH', ); foreach ($git_env as $key) { $value = getenv($key); if (strlen($value)) { $env[$key] = $value; } } $key = 'GIT_PUSH_OPTION_COUNT'; $git_count = getenv($key); if (strlen($git_count)) { $git_count = (int)$git_count; $env[$key] = $git_count; for ($ii = 0; $ii < $git_count; $ii++) { $key = 'GIT_PUSH_OPTION_'.$ii; $env[$key] = getenv($key); } } } $result = array(); foreach ($env as $key => $value) { // In Git, pass anything matching "GIT_*" though. Some of these variables // need to be preserved to allow `git` operations to work properly when // running from commit hooks. if ($this->isGit()) { if (preg_match('/^GIT_/', $key)) { $result[$key] = $value; } } } return $result; } public function supportsBranchComparison() { return $this->isGit(); } public function isReadOnly() { return (bool)$this->getDetail('read-only'); } public function setReadOnly($read_only) { return $this->setDetail('read-only', $read_only); } public function getReadOnlyMessage() { return $this->getDetail('read-only-message'); } public function setReadOnlyMessage($message) { return $this->setDetail('read-only-message', $message); } public function getReadOnlyMessageForDisplay() { $parts = array(); $parts[] = pht( 'This repository is currently in read-only maintenance mode.'); $message = $this->getReadOnlyMessage(); if ($message !== null) { $parts[] = $message; } return implode("\n\n", $parts); } /* -( 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 the URI exists, make sure it's marked as not being disabled. if (isset($custom_map[$builtin_key])) { $uris[$custom_map[$builtin_key]]->setIsDisabled(false); } } // 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); } public function getCloneURIs() { $uris = $this->getURIs(); $clone = array(); foreach ($uris as $uri) { if (!$uri->isBuiltin()) { continue; } if ($uri->getIsDisabled()) { continue; } $io_type = $uri->getEffectiveIoType(); $is_clone = ($io_type == PhabricatorRepositoryURI::IO_READ) || ($io_type == PhabricatorRepositoryURI::IO_READWRITE); if (!$is_clone) { continue; } $clone[] = $uri; } $clone = msort($clone, 'getURIScore'); $clone = array_reverse($clone); return $clone; } public 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, ); // If the view policy of the repository is public, support anonymous HTTP // even if authenticated HTTP is not supported. if ($this->getViewPolicy() === PhabricatorPolicies::POLICY_PUBLIC) { $allow_http = true; } else { $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); // HTTP is not supported for Subversion. if ($this->isSVN()) { $has_http = false; $has_https = false; } $has_ssh = (bool)strlen(PhabricatorEnv::getEnvConfig('phd.user')); $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) { // This is just a dummy value because it can't be empty; we'll force // it to a proper value when using it in the UI. $builtin_uri = "{$protocol}://{$identifier}"; $uris[] = PhabricatorRepositoryURI::initializeNewURI() ->setRepositoryPHID($this->getPHID()) ->attachRepository($this) ->setBuiltinProtocol($protocol) ->setBuiltinIdentifier($identifier) ->setURI($builtin_uri) ->setIsDisabled((int)(!$proto_supported || !$id_supported)); } } return $uris; } public function getClusterRepositoryURIFromBinding( AlmanacBinding $binding) { $protocol = $binding->getAlmanacPropertyValue('protocol'); if ($protocol === null) { $protocol = 'https'; } $iface = $binding->getInterface(); $address = $iface->renderDisplayAddress(); $path = $this->getURI(); return id(new PhutilURI("{$protocol}://{$address}")) ->setPath($path); } public function loadAlmanacService() { $service_phid = $this->getAlmanacServicePHID(); if (!$service_phid) { // No service, so this is a local repository. return null; } $service = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($service_phid)) - ->needBindings(true) + ->needActiveBindings(true) ->needProperties(true) ->executeOne(); if (!$service) { throw new Exception( pht( 'The Almanac service for this repository is invalid or could not '. 'be loaded.')); } $service_type = $service->getServiceImplementation(); if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) { throw new Exception( pht( 'The Almanac service for this repository does not have the correct '. 'service type.')); } return $service; } public function markImporting() { $this->openTransaction(); $this->beginReadLocking(); $repository = $this->reload(); $repository->setDetail('importing', true); $repository->save(); $this->endReadLocking(); $this->saveTransaction(); return $repository; } /* -( Symbols )-------------------------------------------------------------*/ public function getSymbolSources() { return $this->getDetail('symbol-sources', array()); } public function getSymbolLanguages() { return $this->getDetail('symbol-languages', array()); } /* -( Staging )------------------------------------------------------------ */ public function supportsStaging() { return $this->isGit(); } public function getStagingURI() { if (!$this->supportsStaging()) { return null; } return $this->getDetail('staging-uri', null); } /* -( Automation )--------------------------------------------------------- */ public function supportsAutomation() { return $this->isGit(); } public function canPerformAutomation() { if (!$this->supportsAutomation()) { return false; } if (!$this->getAutomationBlueprintPHIDs()) { return false; } return true; } public function getAutomationBlueprintPHIDs() { if (!$this->supportsAutomation()) { return array(); } return $this->getDetail('automation.blueprintPHIDs', array()); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorRepositoryEditor(); } public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, DiffusionPushCapability::CAPABILITY, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case DiffusionPushCapability::CAPABILITY: return $this->getPushPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return false; } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digestForIndex($this->getMarkupText($field)); return "repo:{$hash}"; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { return $this->getDetail('description'); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { require_celerity_resource('phabricator-remarkup-css'); return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return true; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $phid = $this->getPHID(); $this->openTransaction(); $this->delete(); PhabricatorRepositoryURIIndex::updateRepositoryURIs($phid, array()); $books = id(new DivinerBookQuery()) ->setViewer($engine->getViewer()) ->withRepositoryPHIDs(array($phid)) ->execute(); foreach ($books as $book) { $engine->destroyObject($book); } $atoms = id(new DivinerAtomQuery()) ->setViewer($engine->getViewer()) ->withRepositoryPHIDs(array($phid)) ->execute(); foreach ($atoms as $atom) { $engine->destroyObject($atom); } $lfs_refs = id(new PhabricatorRepositoryGitLFSRefQuery()) ->setViewer($engine->getViewer()) ->withRepositoryPHIDs(array($phid)) ->execute(); foreach ($lfs_refs as $ref) { $engine->destroyObject($ref); } $this->saveTransaction(); } /* -( PhabricatorDestructibleCodexInterface )------------------------------ */ public function newDestructibleCodex() { return new PhabricatorRepositoryDestructibleCodex(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The repository name.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('vcs') ->setType('string') ->setDescription( pht('The VCS this repository uses ("git", "hg" or "svn").')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('callsign') ->setType('string') ->setDescription(pht('The repository callsign, if it has one.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('shortName') ->setType('string') ->setDescription(pht('Unique short name, if the repository has one.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Active or inactive status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isImporting') ->setType('bool') ->setDescription( pht( 'True if the repository is importing initial commits.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('almanacServicePHID') ->setType('phid?') ->setDescription( pht( 'The Almanac Service that hosts this repository, if the '. 'repository is clustered.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('refRules') ->setType('map>') ->setDescription( pht( 'The "Fetch" and "Permanent Ref" rules for this repository.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('defaultBranch') ->setType('string?') ->setDescription(pht('Default branch name.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('remarkup') ->setDescription(pht('Repository description.')), ); } public function getFieldValuesForConduit() { $fetch_rules = $this->getFetchRules(); $track_rules = $this->getTrackOnlyRules(); $permanent_rules = $this->getPermanentRefRules(); $fetch_rules = $this->getStringListForConduit($fetch_rules); $track_rules = $this->getStringListForConduit($track_rules); $permanent_rules = $this->getStringListForConduit($permanent_rules); $default_branch = $this->getDefaultBranch(); if (!strlen($default_branch)) { $default_branch = null; } return array( 'name' => $this->getName(), 'vcs' => $this->getVersionControlSystem(), 'callsign' => $this->getCallsign(), 'shortName' => $this->getRepositorySlug(), 'status' => $this->getStatus(), 'isImporting' => (bool)$this->isImporting(), 'almanacServicePHID' => $this->getAlmanacServicePHID(), 'refRules' => array( 'fetchRules' => $fetch_rules, 'trackRules' => $track_rules, 'permanentRefRules' => $permanent_rules, ), 'defaultBranch' => $default_branch, 'description' => array( 'raw' => (string)$this->getDetail('description'), ), ); } private function getStringListForConduit($list) { if (!is_array($list)) { $list = array(); } foreach ($list as $key => $value) { $value = (string)$value; if (!strlen($value)) { unset($list[$key]); } } return array_values($list); } public function getConduitSearchAttachments() { return array( id(new DiffusionRepositoryURIsSearchEngineAttachment()) ->setAttachmentKey('uris'), id(new DiffusionRepositoryMetricsSearchEngineAttachment()) ->setAttachmentKey('metrics'), ); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorRepositoryFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhabricatorRepositoryFerretEngine(); } }