diff --git a/resources/sql/autopatches/20180430.repo_identity.sql b/resources/sql/autopatches/20180430.repo_identity.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20180430.repo_identity.sql @@ -0,0 +1,14 @@ +CREATE TABLE {$NAMESPACE}_repository.repository_identity ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + automaticGuessedUserPHID VARBINARY(64) DEFAULT NULL, + manuallySetUserPHID VARBINARY(64) DEFAULT NULL, + currentEffectiveUserPHID VARBINARY(64) DEFAULT NULL, + identityNameHash BINARY(12) NOT NULL, + identityNameRaw LONGBLOB NOT NULL, + identityNameEncoding VARCHAR(16) DEFAULT NULL COLLATE {$COLLATE_TEXT}, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_identity` (identityNameHash) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE={$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180504.repo_identity.author.sql b/resources/sql/autopatches/20180504.repo_identity.author.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20180504.repo_identity.author.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_identity + ADD COLUMN authorPHID VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20180504.repo_identity.xaction.sql b/resources/sql/autopatches/20180504.repo_identity.xaction.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20180504.repo_identity.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_repository.repository_identitytransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180509.repo_identity.commits.sql b/resources/sql/autopatches/20180509.repo_identity.commits.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20180509.repo_identity.commits.sql @@ -0,0 +1,3 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_commit + ADD COLUMN authorIdentityPHID VARBINARY(64) DEFAULT NULL, + ADD COLUMN committerIdentityPHID VARBINARY(64) DEFAULT NULL; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -814,6 +814,13 @@ 'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php', 'DiffusionHistoryView' => 'applications/diffusion/view/DiffusionHistoryView.php', 'DiffusionHovercardEngineExtension' => 'applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php', + 'DiffusionIdentityAssigneeDatasource' => 'applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php', + 'DiffusionIdentityAssigneeEditField' => 'applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php', + 'DiffusionIdentityAssigneeSearchField' => 'applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php', + 'DiffusionIdentityEditController' => 'applications/diffusion/controller/DiffusionIdentityEditController.php', + 'DiffusionIdentityListController' => 'applications/diffusion/controller/DiffusionIdentityListController.php', + 'DiffusionIdentityUnassignedDatasource' => 'applications/diffusion/typeahead/DiffusionIdentityUnassignedDatasource.php', + 'DiffusionIdentityViewController' => 'applications/diffusion/controller/DiffusionIdentityViewController.php', 'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php', 'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.php', 'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php', @@ -935,6 +942,8 @@ 'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php', 'DiffusionRepositoryFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryFunctionDatasource.php', 'DiffusionRepositoryHistoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryHistoryManagementPanel.php', + 'DiffusionRepositoryIdentityEditor' => 'applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php', + 'DiffusionRepositoryIdentitySearchEngine' => 'applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php', 'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php', 'DiffusionRepositoryManageController' => 'applications/diffusion/controller/DiffusionRepositoryManageController.php', 'DiffusionRepositoryManagePanelsController' => 'applications/diffusion/controller/DiffusionRepositoryManagePanelsController.php', @@ -4078,6 +4087,16 @@ 'PhabricatorRepositoryGitLFSRefQuery' => 'applications/repository/query/PhabricatorRepositoryGitLFSRefQuery.php', 'PhabricatorRepositoryGraphCache' => 'applications/repository/graphcache/PhabricatorRepositoryGraphCache.php', 'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php', + 'PhabricatorRepositoryIdentity' => 'applications/repository/storage/PhabricatorRepositoryIdentity.php', + 'PhabricatorRepositoryIdentityAssignTransaction' => 'applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php', + 'PhabricatorRepositoryIdentityChangeWorker' => 'applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php', + 'PhabricatorRepositoryIdentityEditEngine' => 'applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php', + 'PhabricatorRepositoryIdentityFerretEngine' => 'applications/repository/search/PhabricatorRepositoryIdentityFerretEngine.php', + 'PhabricatorRepositoryIdentityPHIDType' => 'applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php', + 'PhabricatorRepositoryIdentityQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityQuery.php', + 'PhabricatorRepositoryIdentityTransaction' => 'applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php', + 'PhabricatorRepositoryIdentityTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php', + 'PhabricatorRepositoryIdentityTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php', 'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php', 'PhabricatorRepositoryManagementClusterizeWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php', @@ -6144,6 +6163,13 @@ 'DiffusionHistoryTableView' => 'DiffusionHistoryView', 'DiffusionHistoryView' => 'DiffusionView', 'DiffusionHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', + 'DiffusionIdentityAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'DiffusionIdentityAssigneeEditField' => 'PhabricatorTokenizerEditField', + 'DiffusionIdentityAssigneeSearchField' => 'PhabricatorSearchTokenizerField', + 'DiffusionIdentityEditController' => 'DiffusionController', + 'DiffusionIdentityListController' => 'DiffusionController', + 'DiffusionIdentityUnassignedDatasource' => 'PhabricatorTypeaheadDatasource', + 'DiffusionIdentityViewController' => 'DiffusionController', 'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController', 'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController', 'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', @@ -6264,6 +6290,8 @@ 'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryManageController', 'DiffusionRepositoryFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DiffusionRepositoryHistoryManagementPanel' => 'DiffusionRepositoryManagementPanel', + 'DiffusionRepositoryIdentityEditor' => 'PhabricatorApplicationTransactionEditor', + 'DiffusionRepositoryIdentitySearchEngine' => 'PhabricatorApplicationSearchEngine', 'DiffusionRepositoryListController' => 'DiffusionController', 'DiffusionRepositoryManageController' => 'DiffusionController', 'DiffusionRepositoryManagePanelsController' => 'DiffusionRepositoryManageController', @@ -9961,6 +9989,20 @@ 'PhabricatorRepositoryGitLFSRefQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRepositoryGraphCache' => 'Phobject', 'PhabricatorRepositoryGraphStream' => 'Phobject', + 'PhabricatorRepositoryIdentity' => array( + 'PhabricatorRepositoryDAO', + 'PhabricatorPolicyInterface', + 'PhabricatorApplicationTransactionInterface', + ), + 'PhabricatorRepositoryIdentityAssignTransaction' => 'PhabricatorRepositoryIdentityTransactionType', + 'PhabricatorRepositoryIdentityChangeWorker' => 'PhabricatorWorker', + 'PhabricatorRepositoryIdentityEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorRepositoryIdentityFerretEngine' => 'PhabricatorFerretEngine', + 'PhabricatorRepositoryIdentityPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorRepositoryIdentityQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorRepositoryIdentityTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorRepositoryIdentityTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorRepositoryIdentityTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementClusterizeWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow', diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -124,6 +124,15 @@ '(?P[A-Z]+)' => $repository_routes, '(?P[1-9]\d*)' => $repository_routes, + 'identity/' => array( + $this->getQueryRoutePattern() => + 'DiffusionIdentityListController', + $this->getEditRoutePattern('edit/') => + 'DiffusionIdentityEditController', + 'view/(?P[^/]+)/' => + 'DiffusionIdentityViewController', + ), + 'inline/' => array( 'edit/(?P[^/]+)/' => 'DiffusionInlineCommentController', 'preview/(?P[^/]+)/' diff --git a/src/applications/diffusion/controller/DiffusionIdentityEditController.php b/src/applications/diffusion/controller/DiffusionIdentityEditController.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionIdentityEditController.php @@ -0,0 +1,12 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/diffusion/controller/DiffusionIdentityListController.php b/src/applications/diffusion/controller/DiffusionIdentityListController.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionIdentityListController.php @@ -0,0 +1,22 @@ +setController($this) + ->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + id(new PhabricatorRepositoryIdentityEditEngine()) + ->setViewer($this->getViewer()) + ->addActionToCrumbs($crumbs); + + return $crumbs; + } + +} diff --git a/src/applications/diffusion/controller/DiffusionIdentityViewController.php b/src/applications/diffusion/controller/DiffusionIdentityViewController.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionIdentityViewController.php @@ -0,0 +1,136 @@ +getViewer(); + + $id = $request->getURIData('id'); + $identity = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$identity) { + return new Aphront404Response(); + } + + $title = pht('Identity %d', $identity->getID()); + + $curtain = $this->buildCurtain($identity); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($identity->getIdentityShortName()) + ->setHeaderIcon('fa-globe') + ->setPolicyObject($identity); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($identity->getID()); + $crumbs->setBorder(true); + + $timeline = $this->buildTransactionTimeline( + $identity, + new PhabricatorRepositoryIdentityTransactionQuery()); + $timeline->setShouldTerminate(true); + + $properties = $this->buildPropertyList($identity); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $properties, + $timeline, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild( + array( + $view, + )); + } + + private function buildCurtain(PhabricatorRepositoryIdentity $identity) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $identity, + PhabricatorPolicyCapability::CAN_EDIT); + + $id = $identity->getID(); + $edit_uri = $this->getApplicationURI("identity/edit/{$id}/"); + + $curtain = $this->newCurtainView($identity); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Identity')) + ->setHref($edit_uri) + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit)); + + return $curtain; + } + + private function buildPropertyList( + PhabricatorRepositoryIdentity $identity) { + + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $effective_phid = $identity->getCurrentEffectiveUserPHID(); + $automatic_phid = $identity->getAutomaticGuessedUserPHID(); + $manual_phid = $identity->getManuallySetUserPHID(); + + if ($effective_phid) { + $tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setColor('green') + ->setIcon('fa-check') + ->setName('Assigned'); + } else { + $tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setColor('indigo') + ->setIcon('fa-bomb') + ->setName('Unassigned'); + } + $properties->addProperty( + pht('Effective User'), + $this->buildPropertyValue($effective_phid)); + $properties->addProperty( + pht('Automatically Detected User'), + $this->buildPropertyValue($automatic_phid)); + $properties->addProperty( + pht('Manually Set User'), + $this->buildPropertyValue($manual_phid)); + + $header = id(new PHUIHeaderView()) + ->setHeader(array(pht('Identity Assignments'), $tag)); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($properties); + } + + private function buildPropertyValue($value) { + $viewer = $this->getViewer(); + + // TODO: there must be a way to render the fancy "Unassigned" tag + if ($value == DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN) { + return pht(''); + } else if (!$value) { + return null; + } else { + return $viewer->renderHandle($value); + } + } +} diff --git a/src/applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php b/src/applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php @@ -0,0 +1,22 @@ +getIsSingleValue()) { + return new ConduitUserParameterType(); + } else { + return new ConduitUserListParameterType(); + } + } + +} diff --git a/src/applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php b/src/applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php @@ -0,0 +1,26 @@ +setLabel(pht('Assigned To')) + ->setKey('assignee') + ->setDescription(pht('Search for identities by assignee.')), + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Identity Contains')) + ->setKey('match') + ->setDescription(pht('Search for identities by substring.')), + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Is Assigned')) + ->setKey('hasEffectivePHID') + ->setOptions( + pht('(Show All)'), + pht('Show Only Assigned Identities'), + pht('Show Only Unassigned Identities')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['hasEffectivePHID'] !== null) { + $query->withHasEffectivePHID($map['hasEffectivePHID']); + } + + if ($map['match'] !== null) { + $query->withIdentityNameLike($map['match']); + } + + if ($map['assignee'] !== null) { + $query->withAssigneePHIDs($map['assignee']); + } + + return $query; + } + + protected function getURI($path) { + return '/diffusion/identity/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Identities'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $identities, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($identities, 'PhabricatorRepositoryIdentity'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + $list->setUser($viewer); + foreach ($identities as $identity) { + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Identity %d', $identity->getID())) + ->setHeader($identity->getIdentityShortName()) + ->setHref($identity->getURI()) + ->setObject($identity); + + $list->addItem($item); + } + + $result = new PhabricatorApplicationSearchResultView(); + $result->setObjectList($list); + $result->setNoDataString(pht('No Identities found.')); + + return $result; + } + +} diff --git a/src/applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php b/src/applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php @@ -0,0 +1,22 @@ +getUsersFromRequest($request, $key); + } + + protected function newDatasource() { + return new DiffusionIdentityAssigneeDatasource(); + } + + protected function newConduitParameterType() { + return new ConduitUserListParameterType(); + } + +} diff --git a/src/applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php b/src/applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php @@ -0,0 +1,22 @@ + array( + 'name' => pht('Explicitly Unassigned'), + 'summary' => pht('Find results which are not assigned.'), + 'description' => pht( + "This function includes results which have been explicitly ". + "unassigned. Use a query like this to find explicitly ". + "unassigned results:\n\n%s\n\n". + "If you combine this function with other functions, the query will ". + "return results which match the other selectors //or// have no ". + "assignee. For example, this query will find results which are ". + "assigned to `alincoln`, and will also find results which have been ". + "unassigned:\n\n%s", + '> unassigned()', + '> alincoln, unassigned()'), + ), + ); + } + + public function loadResults() { + $results = array( + $this->buildUnassignedResult(), + ); + return $this->filterResultsAgainstTokens($results); + } + + protected function evaluateFunction($function, array $argv_list) { + $results = array(); + + foreach ($argv_list as $argv) { + $results[] = self::FUNCTION_TOKEN; + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $results = array(); + foreach ($argv_list as $argv) { + $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->buildUnassignedResult()); + } + return $results; + } + + private function buildUnassignedResult() { + $name = pht('Unassigned'); + return $this->newFunctionResult() + ->setName($name.' unassigned') + ->setDisplayName($name) + ->setIcon('fa-ban') + ->setPHID('unassigned()') + ->setUnique(true) + ->addAttribute(pht('Select results with no owner.')); + } + +} diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -420,6 +420,11 @@ $user->endWriteLocking(); $user->saveTransaction(); + // Try and match this new address against unclaimed `RepositoryIdentity`s + PhabricatorWorker::scheduleTask( + 'PhabricatorRepositoryIdentityChangeWorker', + array('userPHID' => $user->getPHID())); + return $this; } diff --git a/src/applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php b/src/applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php @@ -0,0 +1,91 @@ +getIdentityShortName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Identity'); + } + + protected function getObjectCreateShortText() { + return pht('Create Identity'); + } + + protected function getObjectName() { + return pht('Identity'); + } + + protected function getEditorURI() { + return '/diffusion/identity/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/diffusion/identity/'; + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function getCreateNewObjectPolicy() { + return PhabricatorPolicies::POLICY_USER; + } + + protected function buildCustomEditFields($object) { + return array( + id(new DiffusionIdentityAssigneeEditField()) + ->setKey('manuallySetUserPHID') + ->setLabel(pht('Assigned To')) + ->setDescription(pht('Override this identity\'s assignment.')) + ->setTransactionType( + PhabricatorRepositoryIdentityAssignTransaction::TRANSACTIONTYPE) + ->setIsCopyable(true) + ->setIsNullable(true) + ->setSingleValue($object->getManuallySetUserPHID()), + + ); + } + +} diff --git a/src/applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php b/src/applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php new file mode 100644 --- /dev/null +++ b/src/applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php @@ -0,0 +1,33 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) {} + +} diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php @@ -0,0 +1,132 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withIdentityNames(array $names) { + $this->identityNames = $names; + return $this; + } + + public function withIdentityNameLike($name_like) { + $this->identityNameLike = $name_like; + return $this; + } + + public function withEmailAddress($address) { + $this->emailAddress = $address; + return $this; + } + + public function withAssigneePHIDs(array $assignees) { + $this->assigneePHIDs = $assignees; + return $this; + } + + public function withHasEffectivePHID($has_effective_phid) { + $this->hasEffectivePHID = $has_effective_phid; + return $this; + } + + public function newResultObject() { + return new PhabricatorRepositoryIdentity(); + } + + protected function getPrimaryTableAlias() { + return 'repository_identity'; + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'repository_identity.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'repository_identity.phid IN (%Ls)', + $this->phids); + } + + if ($this->assigneePHIDs !== null) { + $where[] = qsprintf( + $conn, + 'repository_identity.currentEffectiveUserPHID IN (%Ls)', + $this->assigneePHIDs); + } + + if ($this->hasEffectivePHID !== null) { + + if ($this->hasEffectivePHID) { + $where[] = qsprintf( + $conn, + 'repository_identity.currentEffectiveUserPHID IS NOT NULL'); + } else { + $where[] = qsprintf( + $conn, + 'repository_identity.currentEffectiveUserPHID IS NULL'); + } + } + + if ($this->identityNames !== null) { + $name_hashes = array(); + foreach ($this->identityNames as $name) { + $name_hashes[] = PhabricatorHash::digestForIndex($name); + } + + $where[] = qsprintf( + $conn, + 'repository_identity.identityNameHash IN (%Ls)', + $name_hashes); + } + + if ($this->emailAddress !== null) { + $identity_style = "<{$this->emailAddress}>"; + $where[] = qsprintf( + $conn, + 'repository_identity.identityNameRaw LIKE %<', + $identity_style); + } + + if ($this->identityNameLike != null) { + $where[] = qsprintf( + $conn, + 'repository_identity.identityNameRaw LIKE %~', + $this->identityNameLike); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorDiffusionApplication'; + } + +} diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php @@ -0,0 +1,10 @@ + 'text40', 'mailKey' => 'bytes20', 'authorPHID' => 'phid?', + 'authorIdentityPHID' => 'phid?', + 'committerIdentityPHID' => 'phid?', 'auditStatus' => 'uint32', 'summary' => 'text255', 'importStatus' => 'uint32', diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php new file mode 100644 --- /dev/null +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -0,0 +1,119 @@ + true, + self::CONFIG_BINARY => array( + 'identityNameRaw' => true, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'identityNameHash' => 'bytes12', + 'identityNameEncoding' => 'text16?', + 'automaticGuessedUserPHID' => 'phid?', + 'manuallySetUserPHID' => 'phid?', + 'currentEffectiveUserPHID' => 'phid?', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_identity' => array( + 'columns' => array('identityNameHash'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorRepositoryIdentityPHIDType::TYPECONST; + } + + public function setIdentityName($name_raw) { + $this->setIdentityNameRaw($name_raw); + $this->setIdentityNameHash(PhabricatorHash::digestForIndex($name_raw)); + $this->setIdentityNameEncoding($this->detectEncodingForStorage($name_raw)); + + return $this; + } + + public function getIdentityName() { + return $this->getUTF8StringFromStorage( + $this->getIdentityNameRaw(), + $this->getIdentityNameEncoding()); + } + + public function getIdentityShortName() { + // TODO + return $this->getIdentityName(); + } + + public function getURI() { + return '/diffusion/identity/view/'.$this->getID().'/'; + } + + public function save() { + if ($this->manuallySetUserPHID) { + $this->currentEffectiveUserPHID = $this->manuallySetUserPHID; + } else { + $this->currentEffectiveUserPHID = $this->automaticGuessedUserPHID; + } + + return parent::save(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::getMostOpenPolicy(); + } + + public function hasAutomaticCapability( + $capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new DiffusionRepositoryIdentityEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorRepositoryIdentityTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + + return $timeline; + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php b/src/applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php @@ -0,0 +1,18 @@ +getTaskData(); + $user_phid = idx($task_data, 'userPHID'); + + $user = id(new PhabricatorPeopleQuery()) + ->withPHIDs(array($user_phid)) + ->setViewer($viewer) + ->executeOne(); + + $emails = id(new PhabricatorUserEmail())->loadAllWhere( + 'userPHID = %s ORDER BY address', + $user->getPHID()); + + foreach ($emails as $email) { + $identities = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withEmailAddress($email->getAddress()) + ->execute(); + + foreach ($identities as $identity) { + $identity->setAutomaticGuessedUserPHID($user->getPHID()) + ->save(); + } + } + } + +} diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -66,6 +66,34 @@ $committer = $ref->getCommitter(); $hashes = $ref->getHashes(); + $author_identity = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIdentityNames(array($author)) + ->executeOne(); + + if (!$author_identity) { + $author_identity = id(new PhabricatorRepositoryIdentity()) + ->setAuthorPHID($commit->getPHID()) + ->setIdentityName($author) + ->setAutomaticGuessedUserPHID( + $this->resolveUserPHID($commit, $author)) + ->save(); + } + + $committer_identity = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIdentityNames(array($committer)) + ->executeOne(); + + if (!$committer_identity) { + $committer_identity = id(new PhabricatorRepositoryIdentity()) + ->setAuthorPHID($commit->getPHID()) + ->setIdentityName($committer) + ->setAutomaticGuessedUserPHID( + $this->resolveUserPHID($commit, $committer)) + ->save(); + } + $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); @@ -81,6 +109,8 @@ $data->setCommitDetail('authorName', $ref->getAuthorName()); $data->setCommitDetail('authorEmail', $ref->getAuthorEmail()); + $data->setCommitDetail( + 'authorIdentityPHID', $author_identity->getPHID()); $data->setCommitDetail( 'authorPHID', $this->resolveUserPHID($commit, $author)); @@ -96,6 +126,8 @@ $data->setCommitDetail( 'committerPHID', $this->resolveUserPHID($commit, $committer)); + $data->setCommitDetail( + 'committerIdentityPHID', $committer_identity->getPHID()); } $repository = $this->repository; @@ -133,6 +165,9 @@ $commit->setAuthorPHID($author_phid); } + $commit->setAuthorIdentityPHID($author_identity->getPHID()); + $commit->setCommitterIdentityPHID($committer_identity->getPHID()); + $commit->setSummary($data->getSummary()); $commit->save(); diff --git a/src/applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php @@ -0,0 +1,70 @@ +getManuallySetUserPHID(), null); + } + + public function applyInternalEffects($object, $value) { + $object->setManuallySetUserPHID($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!$old) { + return pht( + '%s assigned this identity to %s.', + $this->renderAuthor(), + $this->renderHandle($new)); + } else if (!$new) { + return pht( + '%s removed %s as the assignee of this identity.', + $this->renderAuthor(), + $this->renderHandle($old)); + } else { + return pht( + '%s changed the assigned user for this identity from %s to %s.', + $this->renderAuthor(), + $this->renderHandle($old), + $this->renderHandle($new)); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $old = $xaction->getOldValue(); + $new = $xaction->getNewValue(); + if (!strlen($new)) { + continue; + } + + if ($new === $old) { + continue; + } + + $assignee_list = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($new)) + ->execute(); + + // TODO: turn this back on when I figure out how to check for + // 'unassigned()' + + // if (!$assignee_list) { + // $errors[] = $this->newInvalidError( + // pht('User "%s" is not a valid user.', + // $new)); + // } + } + return $errors; + } + +} diff --git a/src/applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php b/src/applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php new file mode 100644 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php @@ -0,0 +1,4 @@ +