diff --git a/src/applications/diffusion/data/DiffusionCommitRef.php b/src/applications/diffusion/data/DiffusionCommitRef.php index 5224fc872f..06dd4d5ffb 100644 --- a/src/applications/diffusion/data/DiffusionCommitRef.php +++ b/src/applications/diffusion/data/DiffusionCommitRef.php @@ -1,149 +1,145 @@ getHashes(); $hashes = mpull($hashes, 'newDictionary'); $hashes = array_values($hashes); return array( 'authorEpoch' => $this->authorEpoch, 'authorName' => $this->authorName, 'authorEmail' => $this->authorEmail, 'committerName' => $this->committerName, 'committerEmail' => $this->committerEmail, 'message' => $this->message, 'hashes' => $hashes, ); } public static function newFromDictionary(array $map) { $hashes = idx($map, 'hashes', array()); foreach ($hashes as $key => $hash_map) { $hashes[$key] = DiffusionCommitHash::newFromDictionary($hash_map); } $hashes = array_values($hashes); $author_epoch = idx($map, 'authorEpoch'); $author_name = idx($map, 'authorName'); $author_email = idx($map, 'authorEmail'); $committer_name = idx($map, 'committerName'); $committer_email = idx($map, 'committerEmail'); $message = idx($map, 'message'); return id(new self()) ->setAuthorEpoch($author_epoch) ->setAuthorName($author_name) ->setAuthorEmail($author_email) ->setCommitterName($committer_name) ->setCommitterEmail($committer_email) ->setMessage($message) ->setHashes($hashes); } - public static function newFromConduitResult(array $result) { - return self::newFromDictionary($result); - } - public function setHashes(array $hashes) { assert_instances_of($hashes, 'DiffusionCommitHash'); $this->hashes = $hashes; return $this; } public function getHashes() { return $this->hashes; } public function setAuthorEpoch($author_epoch) { $this->authorEpoch = $author_epoch; return $this; } public function getAuthorEpoch() { return $this->authorEpoch; } public function setCommitterEmail($committer_email) { $this->committerEmail = $committer_email; return $this; } public function getCommitterEmail() { return $this->committerEmail; } public function setCommitterName($committer_name) { $this->committerName = $committer_name; return $this; } public function getCommitterName() { return $this->committerName; } public function setAuthorEmail($author_email) { $this->authorEmail = $author_email; return $this; } public function getAuthorEmail() { return $this->authorEmail; } public function setAuthorName($author_name) { $this->authorName = $author_name; return $this; } public function getAuthorName() { return $this->authorName; } public function setMessage($message) { $this->message = $message; return $this; } public function getMessage() { return $this->message; } public function getAuthor() { return $this->formatUser($this->authorName, $this->authorEmail); } public function getCommitter() { return $this->formatUser($this->committerName, $this->committerEmail); } public function getSummary() { return PhabricatorRepositoryCommitData::summarizeCommitMessage( $this->getMessage()); } private function formatUser($name, $email) { if (strlen($name) && strlen($email)) { return "{$name} <{$email}>"; } else if (strlen($email)) { return $email; } else if (strlen($name)) { return $name; } else { return null; } } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index ef65219b4b..586f1e2d0a 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -1,964 +1,969 @@ repository = $repository; return $this; } public function getRepository($assert_attached = true) { if ($assert_attached) { return $this->assertAttached($this->repository); } return $this->repository; } public function isPartiallyImported($mask) { return (($mask & $this->getImportStatus()) == $mask); } public function isImported() { return $this->isPartiallyImported(self::IMPORTED_ALL); } public function isUnreachable() { return $this->isPartiallyImported(self::IMPORTED_UNREACHABLE); } public function writeImportStatusFlag($flag) { return $this->adjustImportStatusFlag($flag, true); } public function clearImportStatusFlag($flag) { return $this->adjustImportStatusFlag($flag, false); } private function adjustImportStatusFlag($flag, $set) { $conn_w = $this->establishConnection('w'); $table_name = $this->getTableName(); $id = $this->getID(); if ($set) { queryfx( $conn_w, 'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d', $table_name, $flag, $id); $this->setImportStatus($this->getImportStatus() | $flag); } else { queryfx( $conn_w, 'UPDATE %T SET importStatus = (importStatus & ~%d) WHERE id = %d', $table_name, $flag, $id); $this->setImportStatus($this->getImportStatus() & ~$flag); } return $this; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'commitIdentifier' => 'text40', 'authorPHID' => 'phid?', 'authorIdentityPHID' => 'phid?', 'committerIdentityPHID' => 'phid?', 'auditStatus' => 'text32', 'summary' => 'text255', 'importStatus' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'repositoryID' => array( 'columns' => array('repositoryID', 'importStatus'), ), 'authorPHID' => array( 'columns' => array('authorPHID', 'auditStatus', 'epoch'), ), 'repositoryID_2' => array( 'columns' => array('repositoryID', 'epoch'), ), 'key_commit_identity' => array( 'columns' => array('commitIdentifier', 'repositoryID'), 'unique' => true, ), 'key_epoch' => array( 'columns' => array('epoch'), ), 'key_author' => array( 'columns' => array('authorPHID', 'epoch'), ), ), self::CONFIG_NO_MUTATE => array( 'importStatus', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryCommitPHIDType::TYPECONST); } public function loadCommitData() { if (!$this->getID()) { return null; } return id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $this->getID()); } public function attachCommitData( PhabricatorRepositoryCommitData $data = null) { $this->commitData = $data; return $this; } + public function hasCommitData() { + return ($this->commitData !== self::ATTACHABLE) && + ($this->commitData !== null); + } + public function getCommitData() { return $this->assertAttached($this->commitData); } public function attachAudits(array $audits) { assert_instances_of($audits, 'PhabricatorRepositoryAuditRequest'); $this->audits = $audits; return $this; } public function getAudits() { return $this->assertAttached($this->audits); } public function hasAttachedAudits() { return ($this->audits !== self::ATTACHABLE); } public function attachIdentities( PhabricatorRepositoryIdentity $author = null, PhabricatorRepositoryIdentity $committer = null) { $this->authorIdentity = $author; $this->committerIdentity = $committer; return $this; } public function getAuthorIdentity() { return $this->assertAttached($this->authorIdentity); } public function getCommitterIdentity() { return $this->assertAttached($this->committerIdentity); } public function attachAuditAuthority( PhabricatorUser $user, array $authority) { $user_phid = $user->getPHID(); if (!$user->getPHID()) { throw new Exception( pht('You can not attach audit authority for a user with no PHID.')); } $this->auditAuthorityPHIDs[$user_phid] = $authority; return $this; } public function hasAuditAuthority( PhabricatorUser $user, PhabricatorRepositoryAuditRequest $audit) { $user_phid = $user->getPHID(); if (!$user_phid) { return false; } $map = $this->assertAttachedKey($this->auditAuthorityPHIDs, $user_phid); return isset($map[$audit->getAuditorPHID()]); } public function writeOwnersEdges(array $package_phids) { $src_phid = $this->getPHID(); $edge_type = DiffusionCommitHasPackageEdgeType::EDGECONST; $editor = new PhabricatorEdgeEditor(); $dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $src_phid, $edge_type); foreach ($dst_phids as $dst_phid) { $editor->removeEdge($src_phid, $edge_type, $dst_phid); } foreach ($package_phids as $package_phid) { $editor->addEdge($src_phid, $edge_type, $package_phid); } $editor->save(); return $this; } public function getAuditorPHIDsForEdit() { $audits = $this->getAudits(); return mpull($audits, 'getAuditorPHID'); } public function delete() { $data = $this->loadCommitData(); $audits = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere('commitPHID = %s', $this->getPHID()); $this->openTransaction(); if ($data) { $data->delete(); } foreach ($audits as $audit) { $audit->delete(); } $result = parent::delete(); $this->saveTransaction(); return $result; } public function getDateCreated() { // This is primarily to make analysis of commits with the Fact engine work. return $this->getEpoch(); } public function getURI() { return '/'.$this->getMonogram(); } /** * Synchronize a commit's overall audit status with the individual audit * triggers. */ public function updateAuditStatus(array $requests) { assert_instances_of($requests, 'PhabricatorRepositoryAuditRequest'); $any_concern = false; $any_accept = false; $any_need = false; foreach ($requests as $request) { switch ($request->getAuditStatus()) { case PhabricatorAuditStatusConstants::AUDIT_REQUIRED: case PhabricatorAuditStatusConstants::AUDIT_REQUESTED: $any_need = true; break; case PhabricatorAuditStatusConstants::ACCEPTED: $any_accept = true; break; case PhabricatorAuditStatusConstants::CONCERNED: $any_concern = true; break; } } if ($any_concern) { if ($this->isAuditStatusNeedsVerification()) { // If the change is in "Needs Verification", we keep it there as // long as any auditors still have concerns. $status = DiffusionCommitAuditStatus::NEEDS_VERIFICATION; } else { $status = DiffusionCommitAuditStatus::CONCERN_RAISED; } } else if ($any_accept) { if ($any_need) { $status = DiffusionCommitAuditStatus::PARTIALLY_AUDITED; } else { $status = DiffusionCommitAuditStatus::AUDITED; } } else if ($any_need) { $status = DiffusionCommitAuditStatus::NEEDS_AUDIT; } else { $status = DiffusionCommitAuditStatus::NONE; } return $this->setAuditStatus($status); } public function getMonogram() { $repository = $this->getRepository(); $callsign = $repository->getCallsign(); $identifier = $this->getCommitIdentifier(); if ($callsign !== null) { return "r{$callsign}{$identifier}"; } else { $id = $repository->getID(); return "R{$id}:{$identifier}"; } } public function getDisplayName() { $repository = $this->getRepository(); $identifier = $this->getCommitIdentifier(); return $repository->formatCommitName($identifier); } /** * Return a local display name for use in the context of the containing * repository. * * In Git and Mercurial, this returns only a short hash, like "abcdef012345". * See @{method:getDisplayName} for a short name that always includes * repository context. * * @return string Short human-readable name for use inside a repository. */ public function getLocalName() { $repository = $this->getRepository(); $identifier = $this->getCommitIdentifier(); return $repository->formatCommitName($identifier, $local = true); } public function loadIdentities(PhabricatorUser $viewer) { if ($this->authorIdentity !== self::ATTACHABLE) { return $this; } $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIDs(array($this->getID())) ->needIdentities(true) ->executeOne(); $author_identity = $commit->getAuthorIdentity(); $committer_identity = $commit->getCommitterIdentity(); return $this->attachIdentities($author_identity, $committer_identity); } public function hasCommitterIdentity() { return ($this->getCommitterIdentity() !== null); } public function hasAuthorIdentity() { return ($this->getAuthorIdentity() !== null); } public function getCommitterDisplayPHID() { if ($this->hasCommitterIdentity()) { return $this->getCommitterIdentity()->getIdentityDisplayPHID(); } $data = $this->getCommitData(); return $data->getCommitDetail('committerPHID'); } public function getAuthorDisplayPHID() { if ($this->hasAuthorIdentity()) { return $this->getAuthorIdentity()->getIdentityDisplayPHID(); } $data = $this->getCommitData(); return $data->getCommitDetail('authorPHID'); } public function getEffectiveAuthorPHID() { if ($this->hasAuthorIdentity()) { $identity = $this->getAuthorIdentity(); if ($identity->hasEffectiveUser()) { return $identity->getCurrentEffectiveUserPHID(); } } $data = $this->getCommitData(); return $data->getCommitDetail('authorPHID'); } public function getAuditStatusObject() { $status = $this->getAuditStatus(); return DiffusionCommitAuditStatus::newForStatus($status); } public function isAuditStatusNoAudit() { return $this->getAuditStatusObject()->isNoAudit(); } public function isAuditStatusNeedsAudit() { return $this->getAuditStatusObject()->isNeedsAudit(); } public function isAuditStatusConcernRaised() { return $this->getAuditStatusObject()->isConcernRaised(); } public function isAuditStatusNeedsVerification() { return $this->getAuditStatusObject()->isNeedsVerification(); } public function isAuditStatusPartiallyAudited() { return $this->getAuditStatusObject()->isPartiallyAudited(); } public function isAuditStatusAudited() { return $this->getAuditStatusObject()->isAudited(); } public function isPermanentCommit() { return (bool)$this->isPartiallyImported(self::IMPORTED_CLOSEABLE); } public function newCommitAuthorView(PhabricatorUser $viewer) { $author_phid = $this->getAuthorDisplayPHID(); if ($author_phid) { $handles = $viewer->loadHandles(array($author_phid)); return $handles[$author_phid]->renderLink(); } $author = $this->getRawAuthorStringForDisplay(); if (strlen($author)) { return DiffusionView::renderName($author); } return null; } public function newCommitCommitterView(PhabricatorUser $viewer) { $committer_phid = $this->getCommitterDisplayPHID(); if ($committer_phid) { $handles = $viewer->loadHandles(array($committer_phid)); return $handles[$committer_phid]->renderLink(); } $committer = $this->getRawCommitterStringForDisplay(); if (strlen($committer)) { return DiffusionView::renderName($committer); } return null; } public function isAuthorSameAsCommitter() { $author_phid = $this->getAuthorDisplayPHID(); $committer_phid = $this->getCommitterDisplayPHID(); if ($author_phid && $committer_phid) { return ($author_phid === $committer_phid); } if ($author_phid || $committer_phid) { return false; } $author = $this->getRawAuthorStringForDisplay(); $committer = $this->getRawCommitterStringForDisplay(); return ($author === $committer); } private function getRawAuthorStringForDisplay() { $data = $this->getCommitData(); return $data->getAuthorString(); } private function getRawCommitterStringForDisplay() { $data = $this->getCommitData(); return $data->getCommitterString(); } public function newCommitRef(PhabricatorUser $viewer) { $repository = $this->getRepository(); $future = $repository->newConduitFuture( $viewer, 'internal.commit.search', array( 'constraints' => array( 'repositoryPHIDs' => array($repository->getPHID()), 'phids' => array($this->getPHID()), ), )); $result = $future->resolve(); $commit_display = $this->getMonogram(); if (empty($result['data'])) { throw new Exception( pht( 'Unable to retrieve details for commit "%s"!', $commit_display)); } if (count($result['data']) !== 1) { throw new Exception( pht( 'Got too many results (%s) for commit "%s", expected %s.', phutil_count($result['data']), $commit_display, 1)); } $record = head($result['data']); $ref_record = idxv($record, array('fields', 'ref')); if (!$ref_record) { throw new Exception( pht( 'Unable to retrieve CommitRef record for commit "%s".', $commit_display)); } return DiffusionCommitRef::newFromDictionary($ref_record); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getRepository()->getPolicy($capability); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_USER; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Commits inherit the policies of the repository they belong to.'); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( Stuff for serialization )---------------------------------------------- */ /** * NOTE: this is not a complete serialization; only the 'protected' fields are * involved. This is due to ease of (ab)using the Lisk abstraction to get this * done, as well as complexity of the other fields. */ public function toDictionary() { return array( 'repositoryID' => $this->getRepositoryID(), 'phid' => $this->getPHID(), 'commitIdentifier' => $this->getCommitIdentifier(), 'epoch' => $this->getEpoch(), 'authorPHID' => $this->getAuthorPHID(), 'auditStatus' => $this->getAuditStatus(), 'summary' => $this->getSummary(), 'importStatus' => $this->getImportStatus(), ); } public static function newFromDictionary(array $dict) { return id(new PhabricatorRepositoryCommit()) ->loadFromArray($dict); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { return $this->getHarbormasterBuildablePHID(); } public function getHarbormasterBuildablePHID() { return $this->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getRepository()->getPHID(); } public function getBuildVariables() { $results = array(); $results['buildable.commit'] = $this->getCommitIdentifier(); $repo = $this->getRepository(); $results['repository.callsign'] = $repo->getCallsign(); $results['repository.phid'] = $repo->getPHID(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); return $results; } public function getAvailableBuildVariables() { return array( 'buildable.commit' => pht('The commit identifier, if applicable.'), 'repository.callsign' => pht('The callsign of the repository in Phabricator.'), 'repository.phid' => pht('The PHID of the repository in Phabricator.'), 'repository.vcs' => pht('The version control system, either "svn", "hg" or "git".'), 'repository.uri' => pht('The URI to clone or checkout the repository from.'), ); } public function newBuildableEngine() { return new DiffusionBuildableEngine(); } /* -( HarbormasterCircleCIBuildableInterface )----------------------------- */ public function getCircleCIGitHubRepositoryURI() { $repository = $this->getRepository(); $commit_phid = $this->getPHID(); $repository_phid = $repository->getPHID(); if ($repository->isHosted()) { throw new Exception( pht( 'This commit ("%s") is associated with a hosted repository '. '("%s"). Repositories must be imported from GitHub to be built '. 'with CircleCI.', $commit_phid, $repository_phid)); } $remote_uri = $repository->getRemoteURI(); $path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath( $remote_uri); if (!$path) { throw new Exception( pht( 'This commit ("%s") is associated with a repository ("%s") that '. 'with a remote URI ("%s") that does not appear to be hosted on '. 'GitHub. Repositories must be hosted on GitHub to be built with '. 'CircleCI.', $commit_phid, $repository_phid, $remote_uri)); } return $remote_uri; } public function getCircleCIBuildIdentifierType() { return 'revision'; } public function getCircleCIBuildIdentifier() { return $this->getCommitIdentifier(); } /* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */ public function getBuildkiteBranch() { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = $this->getRepository(); $branches = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, DiffusionRequest::newFromDictionary( array( 'repository' => $repository, 'user' => $viewer, )), 'diffusion.branchquery', array( 'contains' => $this->getCommitIdentifier(), 'repository' => $repository->getPHID(), )); if (!$branches) { throw new Exception( pht( 'Commit "%s" is not an ancestor of any branch head, so it can not '. 'be built with Buildkite.', $this->getCommitIdentifier())); } $branch = head($branches); return 'refs/heads/'.$branch['shortName']; } public function getBuildkiteCommit() { return $this->getCommitIdentifier(); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('diffusion.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorCommitCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { // TODO: This should also list auditors, but handling that is a bit messy // right now because we are not guaranteed to have the data. (It should not // include resigned auditors.) return ($phid == $this->getAuthorPHID()); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorAuditEditor(); } public function getApplicationTransactionTemplate() { return new PhabricatorAuditTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new DiffusionCommitFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new DiffusionCommitFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('identifier') ->setType('string') ->setDescription(pht('The commit identifier.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('repositoryPHID') ->setType('phid') ->setDescription(pht('The repository this commit belongs to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('author') ->setType('map') ->setDescription(pht('Information about the commit author.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('committer') ->setType('map') ->setDescription(pht('Information about the committer.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isImported') ->setType('bool') ->setDescription(pht('True if the commit is fully imported.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isUnreachable') ->setType('bool') ->setDescription( pht( 'True if the commit is not the ancestor of any tag, branch, or '. 'ref.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('auditStatus') ->setType('map') ->setDescription(pht('Information about the current audit status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('message') ->setType('string') ->setDescription(pht('The commit message.')), ); } public function getFieldValuesForConduit() { $data = $this->getCommitData(); $author_identity = $this->getAuthorIdentity(); if ($author_identity) { $author_name = $author_identity->getIdentityDisplayName(); $author_email = $author_identity->getIdentityEmailAddress(); $author_raw = $author_identity->getIdentityName(); $author_identity_phid = $author_identity->getPHID(); $author_user_phid = $author_identity->getCurrentEffectiveUserPHID(); } else { $author_name = null; $author_email = null; $author_raw = null; $author_identity_phid = null; $author_user_phid = null; } $committer_identity = $this->getCommitterIdentity(); if ($committer_identity) { $committer_name = $committer_identity->getIdentityDisplayName(); $committer_email = $committer_identity->getIdentityEmailAddress(); $committer_raw = $committer_identity->getIdentityName(); $committer_identity_phid = $committer_identity->getPHID(); $committer_user_phid = $committer_identity->getCurrentEffectiveUserPHID(); } else { $committer_name = null; $committer_email = null; $committer_raw = null; $committer_identity_phid = null; $committer_user_phid = null; } $author_epoch = $data->getAuthorEpoch(); $audit_status = $this->getAuditStatusObject(); return array( 'identifier' => $this->getCommitIdentifier(), 'repositoryPHID' => $this->getRepository()->getPHID(), 'author' => array( 'name' => $author_name, 'email' => $author_email, 'raw' => $author_raw, 'epoch' => $author_epoch, 'identityPHID' => $author_identity_phid, 'userPHID' => $author_user_phid, ), 'committer' => array( 'name' => $committer_name, 'email' => $committer_email, 'raw' => $committer_raw, 'epoch' => (int)$this->getEpoch(), 'identityPHID' => $committer_identity_phid, 'userPHID' => $committer_user_phid, ), 'isUnreachable' => (bool)$this->isUnreachable(), 'isImported' => (bool)$this->isImported(), 'auditStatus' => array( 'value' => $audit_status->getKey(), 'name' => $audit_status->getName(), 'closed' => (bool)$audit_status->getIsClosed(), 'color.ansi' => $audit_status->getAnsiColor(), ), 'message' => $data->getCommitMessage(), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorDraftInterface )------------------------------------------ */ public function newDraftEngine() { return new DiffusionCommitDraftEngine(); } public function getHasDraft(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment()); } public function attachHasDraft(PhabricatorUser $viewer, $has_draft) { $this->drafts[$viewer->getCacheFragment()] = $has_draft; return $this; } /* -( PhabricatorTimelineInterface )--------------------------------------- */ public function newTimelineEngine() { return new DiffusionCommitTimelineEngine(); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommitData.php b/src/applications/repository/storage/PhabricatorRepositoryCommitData.php index 84f9522785..c77da64ec2 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommitData.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommitData.php @@ -1,146 +1,191 @@ false, self::CONFIG_SERIALIZATION => array( 'commitDetails' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'authorName' => 'text', 'commitMessage' => 'text', ), self::CONFIG_KEY_SCHEMA => array( 'commitID' => array( 'columns' => array('commitID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getSummary() { $message = $this->getCommitMessage(); return self::summarizeCommitMessage($message); } public static function summarizeCommitMessage($message) { $max_bytes = id(new PhabricatorRepositoryCommit()) ->getColumnMaximumByteLength('summary'); $summary = phutil_split_lines($message, $retain_endings = false); $summary = head($summary); $summary = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes($max_bytes) ->setMaximumGlyphs(80) ->truncateString($summary); return $summary; } public function getCommitDetail($key, $default = null) { return idx($this->commitDetails, $key, $default); } public function setCommitDetail($key, $value) { $this->commitDetails[$key] = $value; return $this; } public function toDictionary() { return array( 'commitID' => $this->commitID, 'authorName' => $this->authorName, 'commitMessage' => $this->commitMessage, 'commitDetails' => json_encode($this->commitDetails), ); } public static function newFromDictionary(array $dict) { return id(new PhabricatorRepositoryCommitData()) ->loadFromArray($dict); } public function newPublisherHoldReasons() { $holds = $this->getCommitDetail('holdReasons'); // Look for the legacy "autocloseReason" if we don't have a modern list // of hold reasons. if (!$holds) { $old_hold = $this->getCommitDetail('autocloseReason'); if ($old_hold) { $holds = array($old_hold); } } if (!$holds) { $holds = array(); } foreach ($holds as $key => $reason) { $holds[$key] = PhabricatorRepositoryPublisherHoldReason::newForHoldKey( $reason); } return array_values($holds); } public function getAuthorString() { - $author = phutil_string_cast($this->authorName); + $ref = $this->getCommitRef(); + + $author = $ref->getAuthor(); + if (strlen($author)) { + return $author; + } + $author = phutil_string_cast($this->authorName); if (strlen($author)) { return $author; } return null; } public function getAuthorDisplayName() { - return $this->getCommitDetailString('authorName'); + return $this->getCommitRef()->getAuthorName(); } public function getAuthorEmail() { - return $this->getCommitDetailString('authorEmail'); + return $this->getCommitRef()->getAuthorEmail(); } public function getAuthorEpoch() { - $epoch = $this->getCommitDetail('authorEpoch'); + $epoch = $this->getCommitRef()->getAuthorEpoch(); if ($epoch) { return (int)$epoch; } return null; } public function getCommitterString() { + $ref = $this->getCommitRef(); + + $committer = $ref->getCommitter(); + if (strlen($committer)) { + return $committer; + } + return $this->getCommitDetailString('committer'); } public function getCommitterDisplayName() { - return $this->getCommitDetailString('committerName'); + return $this->getCommitRef()->getCommitterName(); } public function getCommitterEmail() { - return $this->getCommitDetailString('committerEmail'); + return $this->getCommitRef()->getCommitterEmail(); } private function getCommitDetailString($key) { $string = $this->getCommitDetail($key); $string = phutil_string_cast($string); if (strlen($string)) { return $string; } return null; } + public function setCommitRef(DiffusionCommitRef $ref) { + $this->setCommitDetail('ref', $ref->newDictionary()); + $this->commitRef = null; + + return $this; + } + + public function getCommitRef() { + if ($this->commitRef === null) { + $map = $this->getCommitDetail('ref', array()); + + if (!is_array($map)) { + $map = array(); + } + + $map = $map + array( + 'authorName' => $this->getCommitDetailString('authorName'), + 'authorEmail' => $this->getCommitDetailString('authorEmail'), + 'authorEpoch' => $this->getCommitDetailString('authorEpoch'), + 'committerName' => $this->getCommitDetailString('committerName'), + 'committerEmail' => $this->getCommitDetailString('committerEmail'), + 'message' => $this->getCommitMessage(), + ); + + $ref = DiffusionCommitRef::newFromDictionary($map); + $this->commitRef = $ref; + } + + return $this->commitRef; + } + } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php index 656b258b40..e1990eaec3 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php @@ -1,132 +1,152 @@ commit) { return $this->commit; } $commit_id = idx($this->getTaskData(), 'commitID'); if (!$commit_id) { throw new PhabricatorWorkerPermanentFailureException( pht('No "%s" in task data.', 'commitID')); } $commit = id(new DiffusionCommitQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($commit_id)) ->executeOne(); if (!$commit) { throw new PhabricatorWorkerPermanentFailureException( pht('Commit "%s" does not exist.', $commit_id)); } if ($commit->isUnreachable()) { throw new PhabricatorWorkerPermanentFailureException( pht( 'Commit "%s" (with internal ID "%s") is no longer reachable from '. 'any branch, tag, or ref in this repository, so it will not be '. 'imported. This usually means that the branch the commit was on '. 'was deleted or overwritten.', $commit->getMonogram(), $commit_id)); } $this->commit = $commit; return $commit; } final protected function doWork() { $commit = $this->loadCommit(); $repository = $commit->getRepository(); $this->repository = $repository; $this->parseCommit($repository, $this->commit); } final protected function shouldQueueFollowupTasks() { return !idx($this->getTaskData(), 'only'); } protected function getImportStepFlag() { return null; } final protected function shouldSkipImportStep() { // If this step has already been performed and this is a "natural" task // which was queued by the normal daemons, decline to do the work again. // This mitigates races if commits are rapidly deleted and revived. $flag = $this->getImportStepFlag(); if (!$flag) { // This step doesn't have an associated flag. return false; } $commit = $this->commit; if (!$commit->isPartiallyImported($flag)) { // This commit doesn't have the flag set yet. return false; } if (!$this->shouldQueueFollowupTasks()) { // This task was queued by administrative tools, so do the work even // if it duplicates existing work. return false; } $this->log( "%s\n", pht( 'Skipping import step; this step was previously completed for '. 'this commit.')); return true; } abstract protected function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit); protected function loadCommitHint(PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = $commit->getRepository(); return id(new DiffusionCommitHintQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withOldCommitIdentifiers(array($commit->getCommitIdentifier())) ->executeOne(); } public function renderForDisplay(PhabricatorUser $viewer) { $suffix = parent::renderForDisplay($viewer); $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIDs(array(idx($this->getTaskData(), 'commitID'))) ->executeOne(); if (!$commit) { return $suffix; } $link = DiffusionView::linkCommit( $commit->getRepository(), $commit->getCommitIdentifier()); return array($link, $suffix); } + final protected function loadCommitData(PhabricatorRepositoryCommit $commit) { + if ($commit->hasCommitData()) { + return $commit->getCommitData(); + } + + $commit_id = $commit->getID(); + + $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( + 'commitID = %d', + $commit_id); + if (!$data) { + $data = id(new PhabricatorRepositoryCommitData()) + ->setCommitID($commit_id); + } + + $commit->attachCommitData($data); + + return $data; + } + final public function getViewer() { return PhabricatorUser::getOmnipotentUser(); } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index 3eead0a26d..1ae1084743 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -1,269 +1,269 @@ shouldSkipImportStep()) { $viewer = $this->getViewer(); $ref = $commit->newCommitRef($viewer); - $this->updateCommitData($ref); + $data = $this->loadCommitData($commit); + $data->setCommitRef($ref); + + $this->updateCommitData($commit, $data); } if ($this->shouldQueueFollowupTasks()) { $this->queueTask( $this->getFollowupTaskClass(), array( 'commitID' => $commit->getID(), ), array( // We queue followup tasks at default priority so that the queue // finishes work it has started before starting more work. If // followups are queued at the same priority level, we do all // message parses first, then all change parses, etc. This makes // progress uneven. See T11677 for discussion. 'priority' => PhabricatorWorker::PRIORITY_DEFAULT, )); } } - final protected function updateCommitData(DiffusionCommitRef $ref) { - $commit = $this->commit; + final protected function updateCommitData( + PhabricatorRepositoryCommit $commit, + PhabricatorRepositoryCommitData $data) { + + $ref = $data->getCommitRef(); + $viewer = $this->getViewer(); + $author = $ref->getAuthor(); $committer = $ref->getCommitter(); $has_committer = (bool)strlen($committer); - $viewer = PhabricatorUser::getOmnipotentUser(); - $identity_engine = id(new DiffusionRepositoryIdentityEngine()) ->setViewer($viewer) ->setSourcePHID($commit->getPHID()); // See T13538. It is possible to synthetically construct a Git commit with // no author and arrive here with NULL for the author value. // This is distinct from a commit with an empty author. Because both these // cases are degenerate and we can't resolve NULL into an identity, cast // NULL to the empty string and merge the flows. $author = phutil_string_cast($author); $author_identity = $identity_engine->newResolvedIdentity($author); if ($has_committer) { $committer_identity = $identity_engine->newResolvedIdentity($committer); } else { $committer_identity = null; } - $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( - 'commitID = %d', - $commit->getID()); - if (!$data) { - $data = new PhabricatorRepositoryCommitData(); - } - $data->setCommitID($commit->getID()); $data->setAuthorName(id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(255) ->truncateString((string)$author)); $data->setCommitDetail('authorEpoch', $ref->getAuthorEpoch()); $data->setCommitDetail('authorName', $ref->getAuthorName()); $data->setCommitDetail('authorEmail', $ref->getAuthorEmail()); $data->setCommitDetail( 'authorIdentityPHID', $author_identity->getPHID()); $data->setCommitDetail( 'authorPHID', $author_identity->getCurrentEffectiveUserPHID()); // See T13538. It is possible to synthetically construct a Git commit with // no message. As above, treat this as though it is the same as the empty // message. $message = $ref->getMessage(); $message = phutil_string_cast($message); $data->setCommitMessage($message); if ($has_committer) { $data->setCommitDetail('committer', $committer); $data->setCommitDetail('committerName', $ref->getCommitterName()); $data->setCommitDetail('committerEmail', $ref->getCommitterEmail()); $data->setCommitDetail( 'committerPHID', $committer_identity->getCurrentEffectiveUserPHID()); $data->setCommitDetail( 'committerIdentityPHID', $committer_identity->getPHID()); $commit->setCommitterIdentityPHID($committer_identity->getPHID()); } $repository = $this->repository; $author_phid = $data->getCommitDetail('authorPHID'); $committer_phid = $data->getCommitDetail('committerPHID'); if ($author_phid != $commit->getAuthorPHID()) { $commit->setAuthorPHID($author_phid); } $commit->setAuthorIdentityPHID($author_identity->getPHID()); $commit->setSummary($data->getSummary()); + $commit->save(); + $data->save(); // If we're publishing this commit, we're going to queue tasks to update // referenced objects (like tasks and revisions). Otherwise, record some // details about why we are not publishing it yet. $publisher = $repository->newPublisher(); if ($publisher->shouldPublishCommit($commit)) { - $actor = PhabricatorUser::getOmnipotentUser(); - $this->closeRevisions($actor, $ref, $commit, $data); - $this->closeTasks($actor, $ref, $commit, $data); + $this->closeRevisions($viewer, $commit); + $this->closeTasks($viewer, $commit); } else { $hold_reasons = $publisher->getCommitHoldReasons($commit); + $data->setCommitDetail('holdReasons', $hold_reasons); + $data->save(); } - $data->save(); - $commit->writeImportStatusFlag( PhabricatorRepositoryCommit::IMPORTED_MESSAGE); } private function closeRevisions( PhabricatorUser $actor, - DiffusionCommitRef $ref, - PhabricatorRepositoryCommit $commit, - PhabricatorRepositoryCommitData $data) { + PhabricatorRepositoryCommit $commit) { $differential = 'PhabricatorDifferentialApplication'; if (!PhabricatorApplication::isClassInstalled($differential)) { return; } $repository = $commit->getRepository(); + $data = $commit->getCommitData(); + $ref = $data->getCommitRef(); $field_query = id(new DiffusionLowLevelCommitFieldsQuery()) ->setRepository($repository) ->withCommitRef($ref); $field_values = $field_query->execute(); $revision_id = idx($field_values, 'revisionID'); if (!$revision_id) { return; } $revision = id(new DifferentialRevisionQuery()) ->setViewer($actor) ->withIDs(array($revision_id)) ->executeOne(); if (!$revision) { return; } // NOTE: This is very old code from when revisions had a single reviewer. // It still powers the "Reviewer (Deprecated)" field in Herald, but should // be removed. if (!empty($field_values['reviewedByPHIDs'])) { $data->setCommitDetail( 'reviewerPHID', head($field_values['reviewedByPHIDs'])); } $match_data = $field_query->getRevisionMatchData(); $data->setCommitDetail('differential.revisionID', $revision_id); $data->setCommitDetail('revisionMatchData', $match_data); $properties = array( 'revisionMatchData' => $match_data, ); $this->queueObjectUpdate($commit, $revision, $properties); } private function closeTasks( PhabricatorUser $actor, - DiffusionCommitRef $ref, - PhabricatorRepositoryCommit $commit, - PhabricatorRepositoryCommitData $data) { + PhabricatorRepositoryCommit $commit) { $maniphest = 'PhabricatorManiphestApplication'; if (!PhabricatorApplication::isClassInstalled($maniphest)) { return; } + $data = $commit->getCommitData(); + $prefixes = ManiphestTaskStatus::getStatusPrefixMap(); $suffixes = ManiphestTaskStatus::getStatusSuffixMap(); $message = $data->getCommitMessage(); $matches = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($message); $task_map = array(); foreach ($matches as $match) { $prefix = phutil_utf8_strtolower($match['prefix']); $suffix = phutil_utf8_strtolower($match['suffix']); $status = idx($suffixes, $suffix); if (!$status) { $status = idx($prefixes, $prefix); } foreach ($match['monograms'] as $task_monogram) { $task_id = (int)trim($task_monogram, 'tT'); $task_map[$task_id] = $status; } } if (!$task_map) { return; } $tasks = id(new ManiphestTaskQuery()) ->setViewer($actor) ->withIDs(array_keys($task_map)) ->execute(); foreach ($tasks as $task_id => $task) { $status = $task_map[$task_id]; $properties = array( 'status' => $status, ); $this->queueObjectUpdate($commit, $task, $properties); } } private function queueObjectUpdate( PhabricatorRepositoryCommit $commit, $object, array $properties) { $this->queueTask( 'DiffusionUpdateObjectAfterCommitWorker', array( 'commitPHID' => $commit->getPHID(), 'objectPHID' => $object->getPHID(), 'properties' => $properties, ), array( 'priority' => PhabricatorWorker::PRIORITY_DEFAULT, )); } }