diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php index a1c10cd5e9..ce411aad35 100644 --- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php +++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php @@ -1,419 +1,442 @@ ownerPHIDs = $phids; return $this; } /** * Query owner authority. This will expand authorities, so a user PHID will * match both packages they own directly and packages owned by a project they * are a member of. */ public function withAuthorityPHIDs(array $phids) { $this->authorityPHIDs = $phids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withRepositoryPHIDs(array $phids) { $this->repositoryPHIDs = $phids; return $this; } public function withPaths(array $paths) { $this->paths = $paths; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withControl($repository_phid, array $paths) { if (empty($this->controlMap[$repository_phid])) { $this->controlMap[$repository_phid] = array(); } foreach ($paths as $path) { $path = (string)$path; $this->controlMap[$repository_phid][$path] = $path; } // We need to load paths to execute control queries. $this->needPaths = true; return $this; } public function withNameNgrams($ngrams) { return $this->withNgramsConstraint( new PhabricatorOwnersPackageNameNgrams(), $ngrams); } public function needPaths($need_paths) { $this->needPaths = $need_paths; return $this; } public function newResultObject() { return new PhabricatorOwnersPackage(); } protected function willExecute() { $this->controlResults = array(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $packages) { $package_ids = mpull($packages, 'getID'); $owners = id(new PhabricatorOwnersOwner())->loadAllWhere( 'packageID IN (%Ld)', $package_ids); $owners = mgroup($owners, 'getPackageID'); foreach ($packages as $package) { $package->attachOwners(idx($owners, $package->getID(), array())); } return $packages; } protected function didFilterPage(array $packages) { $package_ids = mpull($packages, 'getID'); if ($this->needPaths) { $paths = id(new PhabricatorOwnersPath())->loadAllWhere( 'packageID IN (%Ld)', $package_ids); $paths = mgroup($paths, 'getPackageID'); foreach ($packages as $package) { $package->attachPaths(idx($paths, $package->getID(), array())); } } if ($this->controlMap) { foreach ($packages as $package) { // If this package is archived, it's no longer a controlling package // for any path. In particular, it can not force active packages with // weak dominion to give up control. if ($package->isArchived()) { continue; } $this->controlResults[$package->getID()] = $package; } } return $packages; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->shouldJoinOwnersTable()) { $joins[] = qsprintf( $conn, 'JOIN %T o ON o.packageID = p.id', id(new PhabricatorOwnersOwner())->getTableName()); } if ($this->shouldJoinPathTable()) { $joins[] = qsprintf( $conn, 'JOIN %T rpath ON rpath.packageID = p.id', id(new PhabricatorOwnersPath())->getTableName()); } return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->phids !== null) { $where[] = qsprintf( $conn, 'p.phid IN (%Ls)', $this->phids); } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'p.id IN (%Ld)', $this->ids); } if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( $conn, 'rpath.repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } if ($this->authorityPHIDs !== null) { $authority_phids = $this->expandAuthority($this->authorityPHIDs); $where[] = qsprintf( $conn, 'o.userPHID IN (%Ls)', $authority_phids); } if ($this->ownerPHIDs !== null) { $where[] = qsprintf( $conn, 'o.userPHID IN (%Ls)', $this->ownerPHIDs); } if ($this->paths !== null) { $where[] = qsprintf( $conn, 'rpath.path IN (%Ls)', $this->getFragmentsForPaths($this->paths)); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'p.status IN (%Ls)', $this->statuses); } if ($this->controlMap) { $clauses = array(); foreach ($this->controlMap as $repository_phid => $paths) { $fragments = $this->getFragmentsForPaths($paths); $clauses[] = qsprintf( $conn, '(rpath.repositoryPHID = %s AND rpath.path IN (%Ls))', $repository_phid, $fragments); } $where[] = implode(' OR ', $clauses); } return $where; } protected function shouldGroupQueryResultRows() { if ($this->shouldJoinOwnersTable()) { return true; } if ($this->shouldJoinPathTable()) { return true; } return parent::shouldGroupQueryResultRows(); } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name'), 'name' => pht('Name'), ), ) + parent::getBuiltinOrders(); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'name', 'type' => 'string', 'unique' => true, 'reverse' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $package = $this->loadCursorObject($cursor); return array( 'id' => $package->getID(), 'name' => $package->getName(), ); } public function getQueryApplicationClass() { return 'PhabricatorOwnersApplication'; } protected function getPrimaryTableAlias() { return 'p'; } private function shouldJoinOwnersTable() { if ($this->ownerPHIDs !== null) { return true; } if ($this->authorityPHIDs !== null) { return true; } return false; } private function shouldJoinPathTable() { if ($this->repositoryPHIDs !== null) { return true; } if ($this->paths !== null) { return true; } if ($this->controlMap) { return true; } return false; } private function expandAuthority(array $phids) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs($phids) ->execute(); $project_phids = mpull($projects, 'getPHID'); return array_fuse($phids) + array_fuse($project_phids); } private function getFragmentsForPaths(array $paths) { $fragments = array(); foreach ($paths as $path) { foreach (PhabricatorOwnersPackage::splitPath($path) as $fragment) { $fragments[$fragment] = $fragment; } } return $fragments; } /* -( Path Control )------------------------------------------------------- */ /** * Get a list of all packages which control a path or its parent directories, * ordered from weakest to strongest. * * The first package has the most specific claim on the path; the last * package has the most general claim. Multiple packages may have claims of * equal strength, so this ordering is primarily one of usability and * convenience. * * @return list List of controlling packages. */ public function getControllingPackagesForPath( $repository_phid, $path, $ignore_dominion = false) { $path = (string)$path; if (!isset($this->controlMap[$repository_phid][$path])) { throw new PhutilInvalidStateException('withControl'); } if ($this->controlResults === null) { throw new PhutilInvalidStateException('execute'); } $packages = $this->controlResults; $weak_dominion = PhabricatorOwnersPackage::DOMINION_WEAK; $path_fragments = PhabricatorOwnersPackage::splitPath($path); $fragment_count = count($path_fragments); $matches = array(); foreach ($packages as $package_id => $package) { $best_match = null; $include = false; $repository_paths = $package->getPathsForRepository($repository_phid); foreach ($repository_paths as $package_path) { $strength = $package_path->getPathMatchStrength( $path_fragments, $fragment_count); if ($strength > $best_match) { $best_match = $strength; $include = !$package_path->getExcluded(); } } if ($best_match && $include) { if ($ignore_dominion) { $is_weak = false; } else { $is_weak = ($package->getDominion() == $weak_dominion); } $matches[$package_id] = array( 'strength' => $best_match, 'weak' => $is_weak, 'package' => $package, ); } } + // At each strength level, drop weak packages if there are also strong + // packages of the same strength. + $strength_map = igroup($matches, 'strength'); + foreach ($strength_map as $strength => $package_list) { + $any_strong = false; + foreach ($package_list as $package_id => $package) { + if (!$package['weak']) { + $any_strong = true; + break; + } + } + if ($any_strong) { + foreach ($package_list as $package_id => $package) { + if ($package['weak']) { + unset($matches[$package_id]); + } + } + } + } + $matches = isort($matches, 'strength'); $matches = array_reverse($matches); - $first_id = null; + $strongest = null; foreach ($matches as $package_id => $match) { - if ($first_id === null) { - $first_id = $package_id; + if ($strongest === null) { + $strongest = $match['strength']; + } + + if ($match['strength'] === $strongest) { continue; } if ($match['weak']) { unset($matches[$package_id]); } } return array_values(ipull($matches, 'package')); } } diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index 2b22a7a145..5f7b4f28c1 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -1,601 +1,628 @@ setViewer($actor) ->withClasses(array('PhabricatorOwnersApplication')) ->executeOne(); $view_policy = $app->getPolicy( PhabricatorOwnersDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( PhabricatorOwnersDefaultEditCapability::CAPABILITY); return id(new PhabricatorOwnersPackage()) ->setAuditingEnabled(0) ->setAutoReview(self::AUTOREVIEW_NONE) ->setDominion(self::DOMINION_STRONG) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->attachPaths(array()) ->setStatus(self::STATUS_ACTIVE) ->attachOwners(array()) ->setDescription(''); } public static function getStatusNameMap() { return array( self::STATUS_ACTIVE => pht('Active'), self::STATUS_ARCHIVED => pht('Archived'), ); } public static function getAutoreviewOptionsMap() { return array( self::AUTOREVIEW_NONE => array( 'name' => pht('No Autoreview'), ), self::AUTOREVIEW_SUBSCRIBE => array( 'name' => pht('Subscribe to Changes'), ), self::AUTOREVIEW_REVIEW => array( 'name' => pht('Review Changes'), ), self::AUTOREVIEW_BLOCK => array( 'name' => pht('Review Changes (Blocking)'), ), ); } public static function getDominionOptionsMap() { return array( self::DOMINION_STRONG => array( 'name' => pht('Strong (Control All Paths)'), 'short' => pht('Strong'), ), self::DOMINION_WEAK => array( 'name' => pht('Weak (Control Unowned Paths)'), 'short' => pht('Weak'), ), ); } protected function getConfiguration() { return array( // This information is better available from the history table. self::CONFIG_TIMESTAMPS => false, self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort', 'description' => 'text', 'primaryOwnerPHID' => 'phid?', 'auditingEnabled' => 'bool', 'mailKey' => 'bytes20', 'status' => 'text32', 'autoReview' => 'text32', 'dominion' => 'text32', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorOwnersPackagePHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function isArchived() { return ($this->getStatus() == self::STATUS_ARCHIVED); } public function setName($name) { $this->name = $name; return $this; } public function loadOwners() { if (!$this->getID()) { return array(); } return id(new PhabricatorOwnersOwner())->loadAllWhere( 'packageID = %d', $this->getID()); } public function loadPaths() { if (!$this->getID()) { return array(); } return id(new PhabricatorOwnersPath())->loadAllWhere( 'packageID = %d', $this->getID()); } public static function loadAffectedPackages( PhabricatorRepository $repository, array $paths) { if (!$paths) { return array(); } return self::loadPackagesForPaths($repository, $paths); } public static function loadOwningPackages($repository, $path) { if (empty($path)) { return array(); } return self::loadPackagesForPaths($repository, array($path), 1); } private static function loadPackagesForPaths( PhabricatorRepository $repository, array $paths, $limit = 0) { $fragments = array(); foreach ($paths as $path) { foreach (self::splitPath($path) as $fragment) { $fragments[$fragment][$path] = true; } } $package = new PhabricatorOwnersPackage(); $path = new PhabricatorOwnersPath(); $conn = $package->establishConnection('r'); $repository_clause = qsprintf( $conn, 'AND p.repositoryPHID = %s', $repository->getPHID()); // NOTE: The list of $paths may be very large if we're coming from // the OwnersWorker and processing, e.g., an SVN commit which created a new // branch. Break it apart so that it will fit within 'max_allowed_packet', // and then merge results in PHP. $rows = array(); foreach (array_chunk(array_keys($fragments), 128) as $chunk) { $rows[] = queryfx_all( $conn, 'SELECT pkg.id, pkg.dominion, p.excluded, p.path FROM %T pkg JOIN %T p ON p.packageID = pkg.id WHERE p.path IN (%Ls) AND pkg.status IN (%Ls) %Q', $package->getTableName(), $path->getTableName(), $chunk, array( self::STATUS_ACTIVE, ), $repository_clause); } $rows = array_mergev($rows); $ids = self::findLongestPathsPerPackage($rows, $fragments); if (!$ids) { return array(); } arsort($ids); if ($limit) { $ids = array_slice($ids, 0, $limit, $preserve_keys = true); } $ids = array_keys($ids); $packages = $package->loadAllWhere('id in (%Ld)', $ids); $packages = array_select_keys($packages, $ids); return $packages; } public static function loadPackagesForRepository($repository) { $package = new PhabricatorOwnersPackage(); $ids = ipull( queryfx_all( $package->establishConnection('r'), 'SELECT DISTINCT packageID FROM %T WHERE repositoryPHID = %s', id(new PhabricatorOwnersPath())->getTableName(), $repository->getPHID()), 'packageID'); return $package->loadAllWhere('id in (%Ld)', $ids); } public static function findLongestPathsPerPackage(array $rows, array $paths) { // Build a map from each path to all the package paths which match it. $path_hits = array(); $weak = array(); foreach ($rows as $row) { $id = $row['id']; $path = $row['path']; $length = strlen($path); $excluded = $row['excluded']; if ($row['dominion'] === self::DOMINION_WEAK) { $weak[$id] = true; } $matches = $paths[$path]; foreach ($matches as $match => $ignored) { $path_hits[$match][] = array( 'id' => $id, 'excluded' => $excluded, 'length' => $length, ); } } // For each path, process the matching package paths to figure out which // packages actually own it. $path_packages = array(); foreach ($path_hits as $match => $hits) { $hits = isort($hits, 'length'); $packages = array(); foreach ($hits as $hit) { $package_id = $hit['id']; if ($hit['excluded']) { unset($packages[$package_id]); } else { $packages[$package_id] = $hit; } } $path_packages[$match] = $packages; } // Remove packages with weak dominion rules that should cede control to // a more specific package. if ($weak) { foreach ($path_packages as $match => $packages) { + + // Group packages by length. + $length_map = array(); + foreach ($packages as $package_id => $package) { + $length_map[$package['length']][$package_id] = $package; + } + + // For each path length, remove all weak packages if there are any + // strong packages of the same length. This makes sure that if there + // are one or more strong claims on a particular path, only those + // claims stand. + foreach ($length_map as $package_list) { + $any_strong = false; + foreach ($package_list as $package_id => $package) { + if (!isset($weak[$package_id])) { + $any_strong = true; + break; + } + } + + if ($any_strong) { + foreach ($package_list as $package_id => $package) { + if (isset($weak[$package_id])) { + unset($packages[$package_id]); + } + } + } + } + $packages = isort($packages, 'length'); $packages = array_reverse($packages, true); - $first = null; + $best_length = null; foreach ($packages as $package_id => $package) { - // If this is the first package we've encountered, note it and - // continue. We're iterating over the packages from longest to - // shortest match, so this package always has the strongest claim - // on the path. - if ($first === null) { - $first = $package_id; - continue; + // If this is the first package we've encountered, note its length. + // We're iterating over the packages from longest to shortest match, + // so packages of this length always have the best claim on the path. + if ($best_length === null) { + $best_length = $package['length']; } - // If this is the first package we saw, its claim stands even if it - // is a weak package. - if ($first === $package_id) { + // If this package has the same length as the best length, its claim + // stands. + if ($package['length'] === $best_length) { continue; } - // If this is a weak package and not the first package we saw, + // If this is a weak package and does not have the best length, // cede its claim to the stronger package. if (isset($weak[$package_id])) { unset($packages[$package_id]); } } $path_packages[$match] = $packages; } } // For each package that owns at least one path, identify the longest // path it owns. $package_lengths = array(); foreach ($path_packages as $match => $hits) { foreach ($hits as $hit) { $length = $hit['length']; $id = $hit['id']; if (empty($package_lengths[$id])) { $package_lengths[$id] = $length; } else { $package_lengths[$id] = max($package_lengths[$id], $length); } } } return $package_lengths; } public static function splitPath($path) { $trailing_slash = preg_match('@/$@', $path) ? '/' : ''; $path = trim($path, '/'); $parts = explode('/', $path); $result = array(); while (count($parts)) { $result[] = '/'.implode('/', $parts).$trailing_slash; $trailing_slash = '/'; array_pop($parts); } $result[] = '/'; return array_reverse($result); } public function attachPaths(array $paths) { assert_instances_of($paths, 'PhabricatorOwnersPath'); $this->paths = $paths; // Drop this cache if we're attaching new paths. $this->pathRepositoryMap = array(); return $this; } public function getPaths() { return $this->assertAttached($this->paths); } public function getPathsForRepository($repository_phid) { if (isset($this->pathRepositoryMap[$repository_phid])) { return $this->pathRepositoryMap[$repository_phid]; } $map = array(); foreach ($this->getPaths() as $path) { if ($path->getRepositoryPHID() == $repository_phid) { $map[] = $path; } } $this->pathRepositoryMap[$repository_phid] = $map; return $this->pathRepositoryMap[$repository_phid]; } public function attachOwners(array $owners) { assert_instances_of($owners, 'PhabricatorOwnersOwner'); $this->owners = $owners; return $this; } public function getOwners() { return $this->assertAttached($this->owners); } public function getOwnerPHIDs() { return mpull($this->getOwners(), 'getUserPHID'); } public function isOwnerPHID($phid) { if (!$phid) { return false; } $owner_phids = $this->getOwnerPHIDs(); $owner_phids = array_fuse($owner_phids); return isset($owner_phids[$phid]); } public function getMonogram() { return 'O'.$this->getID(); } public function getURI() { // TODO: Move these to "/O123" for consistency. return '/owners/package/'.$this->getID().'/'; } /* -( 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) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isOwnerPHID($viewer->getPHID())) { return true; } break; } return false; } public function describeAutomaticCapability($capability) { return pht('Owners of a package may always view it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorOwnersPackageTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorOwnersPackageTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('owners.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorOwnersCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE packageID = %d', id(new PhabricatorOwnersPath())->getTableName(), $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE packageID = %d', id(new PhabricatorOwnersOwner())->getTableName(), $this->getID()); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the package.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('string') ->setDescription(pht('The package description.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Active or archived status of the package.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('owners') ->setType('list>') ->setDescription(pht('List of package owners.')), ); } public function getFieldValuesForConduit() { $owner_list = array(); foreach ($this->getOwners() as $owner) { $owner_list[] = array( 'ownerPHID' => $owner->getUserPHID(), ); } return array( 'name' => $this->getName(), 'description' => $this->getDescription(), 'status' => $this->getStatus(), 'owners' => $owner_list, ); } public function getConduitSearchAttachments() { return array( id(new PhabricatorOwnersPathsSearchEngineAttachment()) ->setAttachmentKey('paths'), ); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorOwnersPackageFulltextEngine(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new PhabricatorOwnersPackageNameNgrams()) ->setValue($this->getName()), ); } } diff --git a/src/applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php b/src/applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php index 7d24fc6e8d..f21f09b44e 100644 --- a/src/applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php +++ b/src/applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php @@ -1,105 +1,194 @@ 1, 'excluded' => 0, 'dominion' => PhabricatorOwnersPackage::DOMINION_STRONG, 'path' => 'src/', ), array( 'id' => 1, 'excluded' => 1, 'dominion' => PhabricatorOwnersPackage::DOMINION_STRONG, 'path' => 'src/releeph/', ), array( 'id' => 2, 'excluded' => 0, 'dominion' => PhabricatorOwnersPackage::DOMINION_STRONG, 'path' => 'src/releeph/', ), ); $paths = array( 'src/' => array('src/a.php' => true, 'src/releeph/b.php' => true), 'src/releeph/' => array('src/releeph/b.php' => true), ); $this->assertEqual( array( 1 => strlen('src/'), 2 => strlen('src/releeph/'), ), PhabricatorOwnersPackage::findLongestPathsPerPackage($rows, $paths)); $paths = array( 'src/' => array('src/releeph/b.php' => true), 'src/releeph/' => array('src/releeph/b.php' => true), ); $this->assertEqual( array( 2 => strlen('src/releeph/'), ), PhabricatorOwnersPackage::findLongestPathsPerPackage($rows, $paths)); // Test packages with weak dominion. Here, only package #2 should own the // path. Package #1's claim is ceded to Package #2 because it uses weak // rules. Package #2 gets the claim even though it also has weak rules // because there is no more-specific package. $rows = array( array( 'id' => 1, 'excluded' => 0, 'dominion' => PhabricatorOwnersPackage::DOMINION_WEAK, 'path' => 'src/', ), array( 'id' => 2, 'excluded' => 0, 'dominion' => PhabricatorOwnersPackage::DOMINION_WEAK, 'path' => 'src/applications/', ), ); $pvalue = array('src/applications/main/main.c' => true); $paths = array( 'src/' => $pvalue, 'src/applications/' => $pvalue, ); $this->assertEqual( array( 2 => strlen('src/applications/'), ), PhabricatorOwnersPackage::findLongestPathsPerPackage($rows, $paths)); // Now, add a more specific path to Package #1. This tests nested ownership // in packages with weak dominion rules. This time, Package #1 should end // up back on top, with Package #2 cedeing control to its more specific // path. $rows[] = array( 'id' => 1, 'excluded' => 0, 'dominion' => PhabricatorOwnersPackage::DOMINION_WEAK, 'path' => 'src/applications/main/', ); $paths['src/applications/main/'] = $pvalue; $this->assertEqual( array( 1 => strlen('src/applications/main/'), ), PhabricatorOwnersPackage::findLongestPathsPerPackage($rows, $paths)); + // Test cases where multiple packages own the same path, with various + // dominion rules. + + $main_c = 'src/applications/main/main.c'; + + $rules = array( + // All claims strong. + array( + PhabricatorOwnersPackage::DOMINION_STRONG, + PhabricatorOwnersPackage::DOMINION_STRONG, + PhabricatorOwnersPackage::DOMINION_STRONG, + ), + // All claims weak. + array( + PhabricatorOwnersPackage::DOMINION_WEAK, + PhabricatorOwnersPackage::DOMINION_WEAK, + PhabricatorOwnersPackage::DOMINION_WEAK, + ), + // Mixture of strong and weak claims, strong first. + array( + PhabricatorOwnersPackage::DOMINION_STRONG, + PhabricatorOwnersPackage::DOMINION_STRONG, + PhabricatorOwnersPackage::DOMINION_WEAK, + ), + // Mixture of strong and weak claims, weak first. + array( + PhabricatorOwnersPackage::DOMINION_WEAK, + PhabricatorOwnersPackage::DOMINION_STRONG, + PhabricatorOwnersPackage::DOMINION_STRONG, + ), + ); + + foreach ($rules as $rule_idx => $rule) { + $rows = array( + array( + 'id' => 1, + 'excluded' => 0, + 'dominion' => $rule[0], + 'path' => $main_c, + ), + array( + 'id' => 2, + 'excluded' => 0, + 'dominion' => $rule[1], + 'path' => $main_c, + ), + array( + 'id' => 3, + 'excluded' => 0, + 'dominion' => $rule[2], + 'path' => $main_c, + ), + ); + + $paths = array( + $main_c => $pvalue, + ); + + // If one or more packages have strong dominion, they should own the + // path. If not, all the packages with weak dominion should own the + // path. + $strong = array(); + $weak = array(); + foreach ($rule as $idx => $dominion) { + if ($dominion == PhabricatorOwnersPackage::DOMINION_STRONG) { + $strong[] = $idx + 1; + } else { + $weak[] = $idx + 1; + } + } + + if ($strong) { + $expect = $strong; + } else { + $expect = $weak; + } + + $expect = array_fill_keys($expect, strlen($main_c)); + $actual = PhabricatorOwnersPackage::findLongestPathsPerPackage( + $rows, + $paths); + + ksort($actual); + + $this->assertEqual( + $expect, + $actual, + pht('Ruleset "%s" for Identical Ownership', $rule_idx)); + } } }