Path: blob/master/src/applications/owners/storage/PhabricatorOwnersPackage.php
12256 views
<?php12final class PhabricatorOwnersPackage3extends PhabricatorOwnersDAO4implements5PhabricatorPolicyInterface,6PhabricatorApplicationTransactionInterface,7PhabricatorCustomFieldInterface,8PhabricatorDestructibleInterface,9PhabricatorConduitResultInterface,10PhabricatorFulltextInterface,11PhabricatorFerretInterface,12PhabricatorNgramsInterface {1314protected $name;15protected $autoReview;16protected $description;17protected $status;18protected $viewPolicy;19protected $editPolicy;20protected $dominion;21protected $properties = array();22protected $auditingState;23protected $authorityMode;2425private $paths = self::ATTACHABLE;26private $owners = self::ATTACHABLE;27private $customFields = self::ATTACHABLE;28private $pathRepositoryMap = array();2930const STATUS_ACTIVE = 'active';31const STATUS_ARCHIVED = 'archived';3233const AUTOREVIEW_NONE = 'none';34const AUTOREVIEW_SUBSCRIBE = 'subscribe';35const AUTOREVIEW_SUBSCRIBE_ALWAYS = 'subscribe-always';36const AUTOREVIEW_REVIEW = 'review';37const AUTOREVIEW_REVIEW_ALWAYS = 'review-always';38const AUTOREVIEW_BLOCK = 'block';39const AUTOREVIEW_BLOCK_ALWAYS = 'block-always';4041const DOMINION_STRONG = 'strong';42const DOMINION_WEAK = 'weak';4344const AUTHORITY_STRONG = 'strong';45const AUTHORITY_WEAK = 'weak';4647const PROPERTY_IGNORED = 'ignored';4849public static function initializeNewPackage(PhabricatorUser $actor) {50$app = id(new PhabricatorApplicationQuery())51->setViewer($actor)52->withClasses(array('PhabricatorOwnersApplication'))53->executeOne();5455$view_policy = $app->getPolicy(56PhabricatorOwnersDefaultViewCapability::CAPABILITY);57$edit_policy = $app->getPolicy(58PhabricatorOwnersDefaultEditCapability::CAPABILITY);5960return id(new PhabricatorOwnersPackage())61->setAuditingState(PhabricatorOwnersAuditRule::AUDITING_NONE)62->setAutoReview(self::AUTOREVIEW_NONE)63->setDominion(self::DOMINION_STRONG)64->setAuthorityMode(self::AUTHORITY_STRONG)65->setViewPolicy($view_policy)66->setEditPolicy($edit_policy)67->attachPaths(array())68->setStatus(self::STATUS_ACTIVE)69->attachOwners(array())70->setDescription('');71}7273public static function getStatusNameMap() {74return array(75self::STATUS_ACTIVE => pht('Active'),76self::STATUS_ARCHIVED => pht('Archived'),77);78}7980public static function getAutoreviewOptionsMap() {81return array(82self::AUTOREVIEW_NONE => array(83'name' => pht('No Autoreview'),84),85self::AUTOREVIEW_REVIEW => array(86'name' => pht('Review Changes With Non-Owner Author'),87'authority' => true,88),89self::AUTOREVIEW_BLOCK => array(90'name' => pht('Review Changes With Non-Owner Author (Blocking)'),91'authority' => true,92),93self::AUTOREVIEW_SUBSCRIBE => array(94'name' => pht('Subscribe to Changes With Non-Owner Author'),95'authority' => true,96),97self::AUTOREVIEW_REVIEW_ALWAYS => array(98'name' => pht('Review All Changes'),99),100self::AUTOREVIEW_BLOCK_ALWAYS => array(101'name' => pht('Review All Changes (Blocking)'),102),103self::AUTOREVIEW_SUBSCRIBE_ALWAYS => array(104'name' => pht('Subscribe to All Changes'),105),106);107}108109public static function getDominionOptionsMap() {110return array(111self::DOMINION_STRONG => array(112'name' => pht('Strong (Control All Paths)'),113'short' => pht('Strong'),114),115self::DOMINION_WEAK => array(116'name' => pht('Weak (Control Unowned Paths)'),117'short' => pht('Weak'),118),119);120}121122public static function getAuthorityOptionsMap() {123return array(124self::AUTHORITY_STRONG => array(125'name' => pht('Strong (Package Owns Paths)'),126'short' => pht('Strong'),127),128self::AUTHORITY_WEAK => array(129'name' => pht('Weak (Package Watches Paths)'),130'short' => pht('Weak'),131),132);133}134135protected function getConfiguration() {136return array(137// This information is better available from the history table.138self::CONFIG_TIMESTAMPS => false,139self::CONFIG_AUX_PHID => true,140self::CONFIG_SERIALIZATION => array(141'properties' => self::SERIALIZATION_JSON,142),143self::CONFIG_COLUMN_SCHEMA => array(144'name' => 'sort',145'description' => 'text',146'auditingState' => 'text32',147'status' => 'text32',148'autoReview' => 'text32',149'dominion' => 'text32',150'authorityMode' => 'text32',151),152) + parent::getConfiguration();153}154155public function getPHIDType() {156return PhabricatorOwnersPackagePHIDType::TYPECONST;157}158159public function isArchived() {160return ($this->getStatus() == self::STATUS_ARCHIVED);161}162163public function getMustMatchUngeneratedPaths() {164$ignore_attributes = $this->getIgnoredPathAttributes();165return !empty($ignore_attributes['generated']);166}167168public function getPackageProperty($key, $default = null) {169return idx($this->properties, $key, $default);170}171172public function setPackageProperty($key, $value) {173$this->properties[$key] = $value;174return $this;175}176177public function getIgnoredPathAttributes() {178return $this->getPackageProperty(self::PROPERTY_IGNORED, array());179}180181public function setIgnoredPathAttributes(array $attributes) {182return $this->setPackageProperty(self::PROPERTY_IGNORED, $attributes);183}184185public function loadOwners() {186if (!$this->getID()) {187return array();188}189return id(new PhabricatorOwnersOwner())->loadAllWhere(190'packageID = %d',191$this->getID());192}193194public function loadPaths() {195if (!$this->getID()) {196return array();197}198return id(new PhabricatorOwnersPath())->loadAllWhere(199'packageID = %d',200$this->getID());201}202203public static function loadAffectedPackages(204PhabricatorRepository $repository,205array $paths) {206207if (!$paths) {208return array();209}210211return self::loadPackagesForPaths($repository, $paths);212}213214public static function loadAffectedPackagesForChangesets(215PhabricatorRepository $repository,216DifferentialDiff $diff,217array $changesets) {218assert_instances_of($changesets, 'DifferentialChangeset');219220$paths_all = array();221$paths_ungenerated = array();222223foreach ($changesets as $changeset) {224$path = $changeset->getAbsoluteRepositoryPath($repository, $diff);225226$paths_all[] = $path;227228if (!$changeset->isGeneratedChangeset()) {229$paths_ungenerated[] = $path;230}231}232233if (!$paths_all) {234return array();235}236237$packages_all = self::loadAffectedPackages(238$repository,239$paths_all);240241// If there are no generated changesets, we can't possibly need to throw242// away any packages for matching only generated paths. Just return the243// full set of packages.244if ($paths_ungenerated === $paths_all) {245return $packages_all;246}247248$must_match_ungenerated = array();249foreach ($packages_all as $package) {250if ($package->getMustMatchUngeneratedPaths()) {251$must_match_ungenerated[] = $package;252}253}254255// If no affected packages have the "Ignore Generated Paths" flag set, we256// can't possibly need to throw any away.257if (!$must_match_ungenerated) {258return $packages_all;259}260261if ($paths_ungenerated) {262$packages_ungenerated = self::loadAffectedPackages(263$repository,264$paths_ungenerated);265} else {266$packages_ungenerated = array();267}268269// We have some generated paths, and some packages that ignore generated270// paths. Take all the packages which:271//272// - ignore generated paths; and273// - didn't match any ungenerated paths274//275// ...and remove them from the list.276277$must_match_ungenerated = mpull($must_match_ungenerated, null, 'getID');278$packages_ungenerated = mpull($packages_ungenerated, null, 'getID');279$packages_all = mpull($packages_all, null, 'getID');280281foreach ($must_match_ungenerated as $package_id => $package) {282if (!isset($packages_ungenerated[$package_id])) {283unset($packages_all[$package_id]);284}285}286287return $packages_all;288}289290public static function loadOwningPackages($repository, $path) {291if (empty($path)) {292return array();293}294295return self::loadPackagesForPaths($repository, array($path), 1);296}297298private static function loadPackagesForPaths(299PhabricatorRepository $repository,300array $paths,301$limit = 0) {302303$fragments = array();304foreach ($paths as $path) {305foreach (self::splitPath($path) as $fragment) {306$fragments[$fragment][$path] = true;307}308}309310$package = new PhabricatorOwnersPackage();311$path = new PhabricatorOwnersPath();312$conn = $package->establishConnection('r');313314$repository_clause = qsprintf(315$conn,316'AND p.repositoryPHID = %s',317$repository->getPHID());318319// NOTE: The list of $paths may be very large if we're coming from320// the OwnersWorker and processing, e.g., an SVN commit which created a new321// branch. Break it apart so that it will fit within 'max_allowed_packet',322// and then merge results in PHP.323324$rows = array();325foreach (array_chunk(array_keys($fragments), 1024) as $chunk) {326$indexes = array();327foreach ($chunk as $fragment) {328$indexes[] = PhabricatorHash::digestForIndex($fragment);329}330331$rows[] = queryfx_all(332$conn,333'SELECT pkg.id, pkg.dominion, p.excluded, p.path334FROM %T pkg JOIN %T p ON p.packageID = pkg.id335WHERE p.pathIndex IN (%Ls) AND pkg.status IN (%Ls) %Q',336$package->getTableName(),337$path->getTableName(),338$indexes,339array(340self::STATUS_ACTIVE,341),342$repository_clause);343}344$rows = array_mergev($rows);345346$ids = self::findLongestPathsPerPackage($rows, $fragments);347348if (!$ids) {349return array();350}351352arsort($ids);353if ($limit) {354$ids = array_slice($ids, 0, $limit, $preserve_keys = true);355}356$ids = array_keys($ids);357358$packages = $package->loadAllWhere('id in (%Ld)', $ids);359$packages = array_select_keys($packages, $ids);360361return $packages;362}363364public static function loadPackagesForRepository($repository) {365$package = new PhabricatorOwnersPackage();366$ids = ipull(367queryfx_all(368$package->establishConnection('r'),369'SELECT DISTINCT packageID FROM %T WHERE repositoryPHID = %s',370id(new PhabricatorOwnersPath())->getTableName(),371$repository->getPHID()),372'packageID');373374return $package->loadAllWhere('id in (%Ld)', $ids);375}376377public static function findLongestPathsPerPackage(array $rows, array $paths) {378379// Build a map from each path to all the package paths which match it.380$path_hits = array();381$weak = array();382foreach ($rows as $row) {383$id = $row['id'];384$path = $row['path'];385$length = strlen($path);386$excluded = $row['excluded'];387388if ($row['dominion'] === self::DOMINION_WEAK) {389$weak[$id] = true;390}391392$matches = $paths[$path];393foreach ($matches as $match => $ignored) {394$path_hits[$match][] = array(395'id' => $id,396'excluded' => $excluded,397'length' => $length,398);399}400}401402// For each path, process the matching package paths to figure out which403// packages actually own it.404$path_packages = array();405foreach ($path_hits as $match => $hits) {406$hits = isort($hits, 'length');407408$packages = array();409foreach ($hits as $hit) {410$package_id = $hit['id'];411if ($hit['excluded']) {412unset($packages[$package_id]);413} else {414$packages[$package_id] = $hit;415}416}417418$path_packages[$match] = $packages;419}420421// Remove packages with weak dominion rules that should cede control to422// a more specific package.423if ($weak) {424foreach ($path_packages as $match => $packages) {425426// Group packages by length.427$length_map = array();428foreach ($packages as $package_id => $package) {429$length_map[$package['length']][$package_id] = $package;430}431432// For each path length, remove all weak packages if there are any433// strong packages of the same length. This makes sure that if there434// are one or more strong claims on a particular path, only those435// claims stand.436foreach ($length_map as $package_list) {437$any_strong = false;438foreach ($package_list as $package_id => $package) {439if (!isset($weak[$package_id])) {440$any_strong = true;441break;442}443}444445if ($any_strong) {446foreach ($package_list as $package_id => $package) {447if (isset($weak[$package_id])) {448unset($packages[$package_id]);449}450}451}452}453454$packages = isort($packages, 'length');455$packages = array_reverse($packages, true);456457$best_length = null;458foreach ($packages as $package_id => $package) {459// If this is the first package we've encountered, note its length.460// We're iterating over the packages from longest to shortest match,461// so packages of this length always have the best claim on the path.462if ($best_length === null) {463$best_length = $package['length'];464}465466// If this package has the same length as the best length, its claim467// stands.468if ($package['length'] === $best_length) {469continue;470}471472// If this is a weak package and does not have the best length,473// cede its claim to the stronger package.474if (isset($weak[$package_id])) {475unset($packages[$package_id]);476}477}478479$path_packages[$match] = $packages;480}481}482483// For each package that owns at least one path, identify the longest484// path it owns.485$package_lengths = array();486foreach ($path_packages as $match => $hits) {487foreach ($hits as $hit) {488$length = $hit['length'];489$id = $hit['id'];490if (empty($package_lengths[$id])) {491$package_lengths[$id] = $length;492} else {493$package_lengths[$id] = max($package_lengths[$id], $length);494}495}496}497498return $package_lengths;499}500501public static function splitPath($path) {502$result = array(503'/',504);505506$parts = explode('/', $path);507$buffer = '/';508foreach ($parts as $part) {509if (!strlen($part)) {510continue;511}512513$buffer = $buffer.$part.'/';514$result[] = $buffer;515}516517return $result;518}519520public function attachPaths(array $paths) {521assert_instances_of($paths, 'PhabricatorOwnersPath');522$this->paths = $paths;523524// Drop this cache if we're attaching new paths.525$this->pathRepositoryMap = array();526527return $this;528}529530public function getPaths() {531return $this->assertAttached($this->paths);532}533534public function getPathsForRepository($repository_phid) {535if (isset($this->pathRepositoryMap[$repository_phid])) {536return $this->pathRepositoryMap[$repository_phid];537}538539$map = array();540foreach ($this->getPaths() as $path) {541if ($path->getRepositoryPHID() == $repository_phid) {542$map[] = $path;543}544}545546$this->pathRepositoryMap[$repository_phid] = $map;547548return $this->pathRepositoryMap[$repository_phid];549}550551public function attachOwners(array $owners) {552assert_instances_of($owners, 'PhabricatorOwnersOwner');553$this->owners = $owners;554return $this;555}556557public function getOwners() {558return $this->assertAttached($this->owners);559}560561public function getOwnerPHIDs() {562return mpull($this->getOwners(), 'getUserPHID');563}564565public function isOwnerPHID($phid) {566if (!$phid) {567return false;568}569570$owner_phids = $this->getOwnerPHIDs();571$owner_phids = array_fuse($owner_phids);572573return isset($owner_phids[$phid]);574}575576public function getMonogram() {577return 'O'.$this->getID();578}579580public function getURI() {581// TODO: Move these to "/O123" for consistency.582return '/owners/package/'.$this->getID().'/';583}584585public function newAuditingRule() {586return PhabricatorOwnersAuditRule::newFromState($this->getAuditingState());587}588589public function getHasStrongAuthority() {590return ($this->getAuthorityMode() === self::AUTHORITY_STRONG);591}592593/* -( PhabricatorPolicyInterface )----------------------------------------- */594595596public function getCapabilities() {597return array(598PhabricatorPolicyCapability::CAN_VIEW,599PhabricatorPolicyCapability::CAN_EDIT,600);601}602603public function getPolicy($capability) {604switch ($capability) {605case PhabricatorPolicyCapability::CAN_VIEW:606return $this->getViewPolicy();607case PhabricatorPolicyCapability::CAN_EDIT:608return $this->getEditPolicy();609}610}611612public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {613switch ($capability) {614case PhabricatorPolicyCapability::CAN_VIEW:615if ($this->isOwnerPHID($viewer->getPHID())) {616return true;617}618break;619}620621return false;622}623624public function describeAutomaticCapability($capability) {625return pht('Owners of a package may always view it.');626}627628629/* -( PhabricatorApplicationTransactionInterface )------------------------- */630631632public function getApplicationTransactionEditor() {633return new PhabricatorOwnersPackageTransactionEditor();634}635636public function getApplicationTransactionTemplate() {637return new PhabricatorOwnersPackageTransaction();638}639640641/* -( PhabricatorCustomFieldInterface )------------------------------------ */642643644public function getCustomFieldSpecificationForRole($role) {645return PhabricatorEnv::getEnvConfig('owners.fields');646}647648public function getCustomFieldBaseClass() {649return 'PhabricatorOwnersCustomField';650}651652public function getCustomFields() {653return $this->assertAttached($this->customFields);654}655656public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {657$this->customFields = $fields;658return $this;659}660661662/* -( PhabricatorDestructibleInterface )----------------------------------- */663664665public function destroyObjectPermanently(666PhabricatorDestructionEngine $engine) {667668$this->openTransaction();669$conn_w = $this->establishConnection('w');670671queryfx(672$conn_w,673'DELETE FROM %T WHERE packageID = %d',674id(new PhabricatorOwnersPath())->getTableName(),675$this->getID());676677queryfx(678$conn_w,679'DELETE FROM %T WHERE packageID = %d',680id(new PhabricatorOwnersOwner())->getTableName(),681$this->getID());682683$this->delete();684$this->saveTransaction();685}686687688/* -( PhabricatorConduitResultInterface )---------------------------------- */689690691public function getFieldSpecificationsForConduit() {692return array(693id(new PhabricatorConduitSearchFieldSpecification())694->setKey('name')695->setType('string')696->setDescription(pht('The name of the package.')),697id(new PhabricatorConduitSearchFieldSpecification())698->setKey('description')699->setType('string')700->setDescription(pht('The package description.')),701id(new PhabricatorConduitSearchFieldSpecification())702->setKey('status')703->setType('string')704->setDescription(pht('Active or archived status of the package.')),705id(new PhabricatorConduitSearchFieldSpecification())706->setKey('owners')707->setType('list<map<string, wild>>')708->setDescription(pht('List of package owners.')),709id(new PhabricatorConduitSearchFieldSpecification())710->setKey('review')711->setType('map<string, wild>')712->setDescription(pht('Auto review information.')),713id(new PhabricatorConduitSearchFieldSpecification())714->setKey('audit')715->setType('map<string, wild>')716->setDescription(pht('Auto audit information.')),717id(new PhabricatorConduitSearchFieldSpecification())718->setKey('dominion')719->setType('map<string, wild>')720->setDescription(pht('Dominion setting information.')),721id(new PhabricatorConduitSearchFieldSpecification())722->setKey('authority')723->setType('map<string, wild>')724->setDescription(pht('Authority setting information.')),725id(new PhabricatorConduitSearchFieldSpecification())726->setKey('ignored')727->setType('map<string, wild>')728->setDescription(pht('Ignored attribute information.')),729);730}731732public function getFieldValuesForConduit() {733$owner_list = array();734foreach ($this->getOwners() as $owner) {735$owner_list[] = array(736'ownerPHID' => $owner->getUserPHID(),737);738}739740$review_map = self::getAutoreviewOptionsMap();741$review_value = $this->getAutoReview();742if (isset($review_map[$review_value])) {743$review_label = $review_map[$review_value]['name'];744} else {745$review_label = pht('Unknown ("%s")', $review_value);746}747748$review = array(749'value' => $review_value,750'label' => $review_label,751);752753$audit_rule = $this->newAuditingRule();754755$audit = array(756'value' => $audit_rule->getKey(),757'label' => $audit_rule->getDisplayName(),758);759760$dominion_value = $this->getDominion();761$dominion_map = self::getDominionOptionsMap();762if (isset($dominion_map[$dominion_value])) {763$dominion_label = $dominion_map[$dominion_value]['name'];764$dominion_short = $dominion_map[$dominion_value]['short'];765} else {766$dominion_label = pht('Unknown ("%s")', $dominion_value);767$dominion_short = pht('Unknown ("%s")', $dominion_value);768}769770$dominion = array(771'value' => $dominion_value,772'label' => $dominion_label,773'short' => $dominion_short,774);775776777$authority_value = $this->getAuthorityMode();778$authority_map = self::getAuthorityOptionsMap();779if (isset($authority_map[$authority_value])) {780$authority_label = $authority_map[$authority_value]['name'];781$authority_short = $authority_map[$authority_value]['short'];782} else {783$authority_label = pht('Unknown ("%s")', $authority_value);784$authority_short = pht('Unknown ("%s")', $authority_value);785}786787$authority = array(788'value' => $authority_value,789'label' => $authority_label,790'short' => $authority_short,791);792793// Force this to always emit as a JSON object even if empty, never as794// a JSON list.795$ignored = $this->getIgnoredPathAttributes();796if (!$ignored) {797$ignored = (object)array();798}799800return array(801'name' => $this->getName(),802'description' => $this->getDescription(),803'status' => $this->getStatus(),804'owners' => $owner_list,805'review' => $review,806'audit' => $audit,807'dominion' => $dominion,808'authority' => $authority,809'ignored' => $ignored,810);811}812813public function getConduitSearchAttachments() {814return array(815id(new PhabricatorOwnersPathsSearchEngineAttachment())816->setAttachmentKey('paths'),817);818}819820821/* -( PhabricatorFulltextInterface )--------------------------------------- */822823824public function newFulltextEngine() {825return new PhabricatorOwnersPackageFulltextEngine();826}827828829/* -( PhabricatorFerretInterface )----------------------------------------- */830831832public function newFerretEngine() {833return new PhabricatorOwnersPackageFerretEngine();834}835836837/* -( PhabricatorNgramsInterface )----------------------------------------- */838839840public function newNgrams() {841return array(842id(new PhabricatorOwnersPackageNameNgrams())843->setValue($this->getName()),844);845}846847}848849850