diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php index 4e68fb54fe..00beab892c 100644 --- a/src/applications/releeph/storage/ReleephRequest.php +++ b/src/applications/releeph/storage/ReleephRequest.php @@ -1,328 +1,330 @@ getPusherIntent() == self::INTENT_WANT && /** * We use "!= pass" instead of "== want" in case the requestor intent is * not present. In other words, only revert if the requestor explicitly * passed. */ $this->getRequestorIntent() != self::INTENT_PASS; } /** * Will return INTENT_WANT if any pusher wants this request, and no pusher * passes on this request. */ public function getPusherIntent() { $project = $this->loadReleephProject(); if (!$project->getPushers()) { return self::INTENT_WANT; } $found_pusher_want = false; foreach ($this->userIntents as $phid => $intent) { if ($project->isAuthoritativePHID($phid)) { if ($intent == self::INTENT_PASS) { return self::INTENT_PASS; } $found_pusher_want = true; } } if ($found_pusher_want) { return self::INTENT_WANT; } else { return null; } } public function getRequestorIntent() { return idx($this->userIntents, $this->requestUserPHID); } public function getStatus() { return $this->calculateStatus(); } private function calculateStatus() { if ($this->shouldBeInBranch()) { if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_PICKED; } else { return ReleephRequestStatus::STATUS_NEEDS_PICK; } } else { if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_NEEDS_REVERT; } else { $has_been_in_branch = $this->getCommitIdentifier(); // Regardless of why we reverted something, always say reverted if it // was once in the branch. if ($has_been_in_branch) { return ReleephRequestStatus::STATUS_REVERTED; } elseif ($this->getPusherIntent() === ReleephRequest::INTENT_PASS) { // Otherwise, if it has never been in the branch, explicitly say why: return ReleephRequestStatus::STATUS_REJECTED; } elseif ($this->getRequestorIntent() === ReleephRequest::INTENT_WANT) { return ReleephRequestStatus::STATUS_REQUESTED; } else { return ReleephRequestStatus::STATUS_ABANDONED; } } } } /* -( Lisk mechanics )----------------------------------------------------- */ public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, 'userIntents' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( ReleephPHIDTypeRequest::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( Helpful accessors )--------------------------------------------------- */ public function setHandles($handles) { $this->handles = $handles; return $this; } public function getHandles() { return $this->assertAttached($this->handles); } public function getDetail($key, $default = null) { return idx($this->getDetails(), $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getReason() { // Backward compatibility: reason used to be called comments $reason = $this->getDetail('reason'); if (!$reason) { return $this->getDetail('comments'); } return $reason; } public function getSummary() { /** * Instead, you can use: * - getDetail('summary') // the actual user-chosen summary * - getSummaryForDisplay() // falls back to the original commit title * * Or for the fastidious: * - id(new ReleephSummaryFieldSpecification()) * ->setReleephRequest($rr) * ->getValue() // programmatic equivalent to getDetail() */ throw new Exception( "getSummary() has been deprecated!"); } /** * Allow a null summary, and fall back to the title of the commit. */ public function getSummaryForDisplay() { $summary = $this->getDetail('summary'); if (!$summary) { $pr_commit_data = $this->loadPhabricatorRepositoryCommitData(); if ($pr_commit_data) { $message_lines = explode("\n", $pr_commit_data->getCommitMessage()); $message_lines = array_filter($message_lines); $summary = head($message_lines); } } if (!$summary) { $summary = '(no summary given and commit message empty or unparsed)'; } return $summary; } public function loadRequestCommitDiffPHID() { + $phids = array(); $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { - $edges = $this - ->loadPhabricatorRepositoryCommit() - ->loadRelativeEdges(PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV); - return head(array_keys($edges)); + $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $commit->getPHID(), + PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV); } + + return head($phids); } /* -( Loading external objects )------------------------------------------- */ public function loadReleephBranch() { return $this->loadOneRelative( new ReleephBranch(), 'id', 'getBranchID'); } public function loadReleephProject() { return $this->loadReleephBranch()->loadReleephProject(); } public function loadPhabricatorRepositoryCommit() { return $this->loadOneRelative( new PhabricatorRepositoryCommit(), 'phid', 'getRequestCommitPHID'); } public function loadPhabricatorRepositoryCommitData() { $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { return $commit->loadOneRelative( new PhabricatorRepositoryCommitData(), 'commitID'); } } public function loadDifferentialRevision() { $diff_phid = $this->loadRequestCommitDiffPHID(); if (!$diff_phid) { return null; } return $this->loadOneRelative( new DifferentialRevision(), 'phid', 'loadRequestCommitDiffPHID'); } /* -( State change helpers )----------------------------------------------- */ public function setUserIntent(PhabricatorUser $user, $intent) { $this->userIntents[$user->getPHID()] = $intent; return $this; } /* -( Migrating to status-less ReleephRequests )--------------------------- */ protected function didReadData() { if ($this->userIntents === null) { $this->userIntents = array(); } } public function setStatus($value) { throw new Exception('`status` is now deprecated!'); } /* -( Make magic Lisk methods private )------------------------------------ */ private function setUserIntents(array $ar) { return parent::setUserIntents($ar); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('releeph.fields'); } public function getCustomFieldBaseClass() { return 'ReleephFieldSpecification'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/infrastructure/edges/query/PhabricatorEdgeQuery.php b/src/infrastructure/edges/query/PhabricatorEdgeQuery.php index f42af09848..675354bedc 100644 --- a/src/infrastructure/edges/query/PhabricatorEdgeQuery.php +++ b/src/infrastructure/edges/query/PhabricatorEdgeQuery.php @@ -1,310 +1,309 @@ withSourcePHIDs(array($src)) * ->withEdgeTypes(array($type)) * ->execute(); * * For more information on edges, see @{article:Using Edges}. * * @task config Configuring the Query * @task exec Executing the Query * @task internal Internal */ final class PhabricatorEdgeQuery extends PhabricatorQuery { private $sourcePHIDs; private $destPHIDs; private $edgeTypes; private $resultSet; private $needEdgeData; /* -( Configuring the Query )---------------------------------------------- */ /** * Find edges originating at one or more source PHIDs. You MUST provide this * to execute an edge query. * * @param list List of source PHIDs. * @return this * * @task config */ public function withSourcePHIDs(array $source_phids) { $this->sourcePHIDs = $source_phids; return $this; } /** * Find edges terminating at one or more destination PHIDs. * * @param list List of destination PHIDs. * @return this * */ public function withDestinationPHIDs(array $dest_phids) { $this->destPHIDs = $dest_phids; return $this; } /** * Find edges of specific types. * * @param list List of PhabricatorEdgeConfig type constants. * @return this * * @task config */ public function withEdgeTypes(array $types) { $this->edgeTypes = $types; return $this; } /** * When loading edges, also load edge data. * * @param bool True to load edge data. * @return this * * @task config */ public function needEdgeData($need) { $this->needEdgeData = $need; return $this; } /* -( Executing the Query )------------------------------------------------ */ /** * Convenience method for loading destination PHIDs with one source and one * edge type. Equivalent to building a full query, but simplifies a common * use case. * * @param phid Source PHID. * @param const Edge type. * @return list List of destination PHIDs. */ public static function loadDestinationPHIDs($src_phid, $edge_type) { $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($src_phid)) ->withEdgeTypes(array($edge_type)) ->execute(); return array_keys($edges[$src_phid][$edge_type]); } /** * Convenience method for loading a single edge's metadata for * a given source, destination, and edge type. Returns null * if the edge does not exist or does not have metadata. Builds * and immediately executes a full query. * * @param phid Source PHID. * @param const Edge type. * @param phid Destination PHID. * @return wild Edge annotation (or null). */ public static function loadSingleEdgeData($src_phid, $edge_type, $dest_phid) { $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($src_phid)) ->withEdgeTypes(array($edge_type)) ->withDestinationPHIDs(array($dest_phid)) ->needEdgeData(true) ->execute(); if (isset($edges[$src_phid][$edge_type][$dest_phid]['data'])) { return $edges[$src_phid][$edge_type][$dest_phid]['data']; } return null; } /** * Load specified edges. * * @task exec */ public function execute() { if (!$this->sourcePHIDs) { throw new Exception( "You must use withSourcePHIDs() to query edges."); } $sources = phid_group_by_type($this->sourcePHIDs); $result = array(); // When a query specifies types, make sure we return data for all queried - // types. This is mostly to make sure PhabricatorLiskDAO->attachEdges() - // gets some data, so that getEdges() doesn't throw later. + // types. if ($this->edgeTypes) { foreach ($this->sourcePHIDs as $phid) { foreach ($this->edgeTypes as $type) { $result[$phid][$type] = array(); } } } foreach ($sources as $type => $phids) { $conn_r = PhabricatorEdgeConfig::establishConnection($type, 'r'); $where = $this->buildWhereClause($conn_r); $order = $this->buildOrderClause($conn_r); $edges = queryfx_all( $conn_r, 'SELECT edge.* FROM %T edge %Q %Q', PhabricatorEdgeConfig::TABLE_NAME_EDGE, $where, $order); if ($this->needEdgeData) { $data_ids = array_filter(ipull($edges, 'dataID')); $data_map = array(); if ($data_ids) { $data_rows = queryfx_all( $conn_r, 'SELECT edgedata.* FROM %T edgedata WHERE id IN (%Ld)', PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA, $data_ids); foreach ($data_rows as $row) { $data_map[$row['id']] = idx( json_decode($row['data'], true), 'data'); } } foreach ($edges as $key => $edge) { $edges[$key]['data'] = idx($data_map, $edge['dataID'], array()); } } foreach ($edges as $edge) { $result[$edge['src']][$edge['type']][$edge['dst']] = $edge; } } $this->resultSet = $result; return $result; } /** * Convenience function for selecting edge destination PHIDs after calling * execute(). * * Returns a flat list of PHIDs matching the provided source PHID and type * filters. By default, the filters are empty so all PHIDs will be returned. * For example, if you're doing a batch query from several sources, you might * write code like this: * * $query = new PhabricatorEdgeQuery(); * $query->setViewer($viewer); * $query->withSourcePHIDs(mpull($objects, 'getPHID')); * $query->withEdgeTypes(array($some_type)); * $query->execute(); * * // Gets all of the destinations. * $all_phids = $query->getDestinationPHIDs(); * $handles = id(new PhabricatorHandleQuery()) * ->setViewer($viewer) * ->withPHIDs($all_phids) * ->execute(); * * foreach ($objects as $object) { * // Get all of the destinations for the given object. * $dst_phids = $query->getDestinationPHIDs(array($object->getPHID())); * $object->attachHandles(array_select_keys($handles, $dst_phids)); * } * * @param list? List of PHIDs to select, or empty to select all. * @param list? List of edge types to select, or empty to select all. * @return list List of matching destination PHIDs. */ public function getDestinationPHIDs( array $src_phids = array(), array $types = array()) { if ($this->resultSet === null) { throw new Exception( "You must execute() a query before you you can getDestinationPHIDs()."); } $src_phids = array_fill_keys($src_phids, true); $types = array_fill_keys($types, true); $result_phids = array(); foreach ($this->resultSet as $src => $edges_by_type) { if ($src_phids && empty($src_phids[$src])) { continue; } foreach ($edges_by_type as $type => $edges_by_dst) { if ($types && empty($types[$type])) { continue; } foreach ($edges_by_dst as $dst => $edge) { $result_phids[$dst] = true; } } } return array_keys($result_phids); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function buildWhereClause($conn_r) { $where = array(); if ($this->sourcePHIDs) { $where[] = qsprintf( $conn_r, 'edge.src IN (%Ls)', $this->sourcePHIDs); } if ($this->edgeTypes) { $where[] = qsprintf( $conn_r, 'edge.type IN (%Ls)', $this->edgeTypes); } if ($this->destPHIDs) { // potentially complain if $this->edgeType was not set $where[] = qsprintf( $conn_r, 'edge.dst IN (%Ls)', $this->destPHIDs); } return $this->formatWhereClause($where); } /** * @task internal */ private function buildOrderClause($conn_r) { return 'ORDER BY edge.dateCreated DESC, edge.seq ASC'; } } diff --git a/src/infrastructure/storage/lisk/LiskDAOSet.php b/src/infrastructure/storage/lisk/LiskDAOSet.php index ad2ad452ca..a554a19b62 100644 --- a/src/infrastructure/storage/lisk/LiskDAOSet.php +++ b/src/infrastructure/storage/lisk/LiskDAOSet.php @@ -1,113 +1,89 @@ addToSet($author); * foreach ($reviewers as $reviewer) { * $users->addToSet($reviewer); * } * foreach ($ccs as $cc) { * $users->addToSet($cc); * } * // Preload e-mails of all involved users and return e-mails of author. * $author_emails = $author->loadRelatives( * new PhabricatorUserEmail(), * 'userPHID', * 'getPHID'); */ final class LiskDAOSet { private $daos = array(); private $relatives = array(); - private $edges = array(); private $subsets = array(); public function addToSet(LiskDAO $dao) { - if ($this->relatives || $this->edges) { + if ($this->relatives) { throw new Exception("Don't call addToSet() after loading data!"); } $this->daos[] = $dao; $dao->putInSet($this); return $this; } /** * The main purpose of this method is to break cyclic dependency. * It removes all objects from this set and all subsets created by it. */ final public function clearSet() { $this->daos = array(); $this->relatives = array(); - $this->edges = array(); foreach ($this->subsets as $set) { $set->clearSet(); } $this->subsets = array(); return $this; } /** * See @{method:LiskDAO::loadRelatives}. */ public function loadRelatives( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { $relatives = &$this->relatives[ get_class($object)."-{$foreign_column}-{$key_method}-{$where}"]; if ($relatives === null) { $ids = array(); foreach ($this->daos as $dao) { $id = $dao->$key_method(); if ($id !== null) { $ids[$id] = $id; } } if (!$ids) { $relatives = array(); } else { $set = new LiskDAOSet(); $this->subsets[] = $set; $relatives = $object->putInSet($set)->loadAllWhere( '%C IN (%Ls) %Q', $foreign_column, $ids, ($where != '' ? 'AND '.$where : '')); $relatives = mgroup($relatives, 'get'.$foreign_column); } } return $relatives; } - public function loadRelativeEdges($type) { - $edges = &$this->edges[$type]; - - if ($edges === null) { - if (!$this->daos) { - $edges = array(); - } else { - assert_instances_of($this->daos, 'PhabricatorLiskDAO'); - $phids = mpull($this->daos, 'getPHID', 'getPHID'); - $edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs($phids) - ->withEdgeTypes(array($type)) - ->execute(); - foreach ($this->daos as $dao) { - $dao->attachEdges($edges[$dao->getPHID()]); - } - } - } - - return $edges; - } - } diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index ae28fca4c7..cca5c3accf 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -1,222 +1,178 @@ "; -/* -( Managing Edges )----------------------------------------------------- */ - - - /** - * @task edges - */ - public function attachEdges(array $edges) { - foreach ($edges as $type => $type_edges) { - $this->edges[$type] = $type_edges; - } - return $this; - } - - - /** - * @task edges - */ - public function getEdges($type) { - return $this->assertAttachedKey($this->edges, $type); - } - - - /** - * @task edges - */ - public function loadRelativeEdges($type) { - if (!$this->getInSet()) { - id(new LiskDAOSet())->addToSet($this); - } - $this->getInSet()->loadRelativeEdges($type); - return $this->getEdges($type); - } - - - /** - * @task edges - */ - public function getEdgePHIDs($type) { - return ipull($this->getEdges($type), 'dst'); - } - - /* -( Configuring Storage )------------------------------------------------ */ /** * @task config */ public static function pushStorageNamespace($namespace) { self::$namespaceStack[] = $namespace; } /** * @task config */ public static function popStorageNamespace() { array_pop(self::$namespaceStack); } /** * @task config */ public static function getDefaultStorageNamespace() { return PhabricatorEnv::getEnvConfig('storage.default-namespace'); } /** * @task config */ public static function getStorageNamespace() { $namespace = end(self::$namespaceStack); if (!strlen($namespace)) { $namespace = self::getDefaultStorageNamespace(); } if (!strlen($namespace)) { throw new Exception("No storage namespace configured!"); } return $namespace; } /** * @task config */ public function establishLiveConnection($mode) { $namespace = self::getStorageNamespace(); $conf = PhabricatorEnv::newObjectFromConfig( 'mysql.configuration-provider', array($this, $mode, $namespace)); return PhabricatorEnv::newObjectFromConfig( 'mysql.implementation', array( array( 'user' => $conf->getUser(), 'pass' => $conf->getPassword(), 'host' => $conf->getHost(), 'port' => $conf->getPort(), 'database' => $conf->getDatabase(), 'retries' => 3, ), )); } /** * @task config */ public function getTableName() { $str = 'phabricator'; $len = strlen($str); $class = strtolower(get_class($this)); if (!strncmp($class, $str, $len)) { $class = substr($class, $len); } $app = $this->getApplicationName(); if (!strncmp($class, $app, strlen($app))) { $class = substr($class, strlen($app)); } if (strlen($class)) { return $app.'_'.$class; } else { return $app; } } /** * @task config */ abstract public function getApplicationName(); protected function getConnectionNamespace() { return self::getStorageNamespace().'_'.$this->getApplicationName(); } /** * Break a list of escaped SQL statement fragments (e.g., VALUES lists for * INSERT, previously built with @{function:qsprintf}) into chunks which will * fit under the MySQL 'max_allowed_packet' limit. * * Chunks are glued together with `$glue`, by default ", ". * * If a statement is too large to fit within the limit, it is broken into * its own chunk (but might fail when the query executes). */ public static function chunkSQL( array $fragments, $glue = ', ', $limit = null) { if ($limit === null) { // NOTE: Hard-code this at 1MB for now, minus a 10% safety buffer. // Eventually we could query MySQL or let the user configure it. $limit = (int)((1024 * 1024) * 0.90); } $result = array(); $chunk = array(); $len = 0; $glue_len = strlen($glue); foreach ($fragments as $fragment) { $this_len = strlen($fragment); if ($chunk) { // Chunks after the first also imply glue. $this_len += $glue_len; } if ($len + $this_len <= $limit) { $len += $this_len; $chunk[] = $fragment; } else { if ($chunk) { $result[] = $chunk; } $len = strlen($fragment); $chunk = array($fragment); } } if ($chunk) { $result[] = $chunk; } foreach ($result as $key => $fragment_list) { $result[$key] = implode($glue, $fragment_list); } return $result; } protected function assertAttached($property) { if ($property === self::ATTACHABLE) { throw new PhabricatorDataNotAttachedException($this); } return $property; } protected function assertAttachedKey($value, $key) { $this->assertAttached($value); if (!array_key_exists($key, $value)) { throw new PhabricatorDataNotAttachedException($this); } return $value[$key]; } }