diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index 84764c1426..6f5d220c0b 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -1,935 +1,997 @@ auditReasonMap[$phid])) { $this->auditReasonMap[$phid] = array(); } $this->auditReasonMap[$phid][] = $reason; return $this; } private function getAuditReasons($phid) { if (isset($this->auditReasonMap[$phid])) { return $this->auditReasonMap[$phid]; } if ($this->getIsHeraldEditor()) { $name = 'herald'; } else { $name = $this->getActor()->getUsername(); } return array('Added by '.$name.'.'); } public function setRawPatch($patch) { $this->rawPatch = $patch; return $this; } public function getRawPatch() { return $this->rawPatch; } public function getEditorApplicationClass() { return 'PhabricatorAuditApplication'; } public function getEditorObjectsDescription() { return pht('Audits'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorAuditTransaction::TYPE_COMMIT; + $types[] = PhabricatorAuditTransaction::TYPE_INLINEDONE; // TODO: These will get modernized eventually, but that can happen one // at a time later on. $types[] = PhabricatorAuditActionConstants::ACTION; $types[] = PhabricatorAuditActionConstants::INLINE; $types[] = PhabricatorAuditActionConstants::ADD_AUDITORS; return $types; } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::INLINE: return $xaction->hasComment(); } return parent::transactionHasEffect($object, $xaction); } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditTransaction::TYPE_COMMIT: return null; case PhabricatorAuditActionConstants::ADD_AUDITORS: // TODO: For now, just record the added PHIDs. Eventually, turn these // into real edge transactions, probably? return array(); } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditActionConstants::ADD_AUDITORS: case PhabricatorAuditTransaction::TYPE_COMMIT: + case PhabricatorAuditTransaction::TYPE_INLINEDONE: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditActionConstants::ADD_AUDITORS: case PhabricatorAuditTransaction::TYPE_COMMIT: + case PhabricatorAuditTransaction::TYPE_INLINEDONE: return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditTransaction::TYPE_COMMIT: return; case PhabricatorAuditActionConstants::INLINE: $reply = $xaction->getComment()->getReplyToComment(); if ($reply && !$reply->getHasReplies()) { $reply->setHasReplies(1)->save(); } return; + case PhabricatorAuditTransaction::TYPE_INLINEDONE: + $table = new PhabricatorAuditTransactionComment(); + $conn_w = $table->establishConnection('w'); + foreach ($xaction->getNewValue() as $phid => $state) { + queryfx( + $conn_w, + 'UPDATE %T SET fixedState = %s WHERE phid = %s', + $table->getTableName(), + $state, + $phid); + } + return; case PhabricatorAuditActionConstants::ADD_AUDITORS: $new = $xaction->getNewValue(); if (!is_array($new)) { $new = array(); } $old = $xaction->getOldValue(); if (!is_array($old)) { $old = array(); } $add = array_diff_key($new, $old); $actor = $this->requireActor(); $requests = $object->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); foreach ($add as $phid) { if (isset($requests[$phid])) { continue; } if ($this->getIsHeraldEditor()) { $audit_requested = $xaction->getMetadataValue('auditStatus'); $audit_reason_map = $xaction->getMetadataValue('auditReasonMap'); $audit_reason = $audit_reason_map[$phid]; } else { $audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED; $audit_reason = $this->getAuditReasons($phid); } $requests[] = id (new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($object->getPHID()) ->setAuditorPHID($phid) ->setAuditStatus($audit_requested) ->setAuditReasons($audit_reason) ->save(); } $object->attachAudits($requests); return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // Load auditors explicitly; we may not have them if the caller was a // generic piece of infrastructure. $commit = id(new DiffusionCommitQuery()) ->setViewer($this->requireActor()) ->withIDs(array($object->getID())) ->needAuditRequests(true) ->executeOne(); if (!$commit) { throw new Exception( pht('Failed to load commit during transaction finalization!')); } $object->attachAudits($commit->getAudits()); $status_concerned = PhabricatorAuditStatusConstants::CONCERNED; $status_closed = PhabricatorAuditStatusConstants::CLOSED; $status_resigned = PhabricatorAuditStatusConstants::RESIGNED; $status_accepted = PhabricatorAuditStatusConstants::ACCEPTED; $status_concerned = PhabricatorAuditStatusConstants::CONCERNED; $actor_phid = $this->getActingAsPHID(); $actor_is_author = ($object->getAuthorPHID()) && ($actor_phid == $object->getAuthorPHID()); $import_status_flag = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $import_status_flag = PhabricatorRepositoryCommit::IMPORTED_HERALD; break; case PhabricatorAuditActionConstants::ACTION: $new = $xaction->getNewValue(); switch ($new) { case PhabricatorAuditActionConstants::CLOSE: // "Close" means wipe out all the concerns. $requests = $object->getAudits(); foreach ($requests as $request) { if ($request->getAuditStatus() == $status_concerned) { $request ->setAuditStatus($status_closed) ->save(); } } break; case PhabricatorAuditActionConstants::RESIGN: $requests = $object->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); $actor_request = idx($requests, $actor_phid); // If the actor doesn't currently have a relationship to the // commit, add one explicitly. For example, this allows members // of a project to resign from a commit and have it drop out of // their queue. if (!$actor_request) { $actor_request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($object->getPHID()) ->setAuditorPHID($actor_phid); $requests[] = $actor_request; $object->attachAudits($requests); } $actor_request ->setAuditStatus($status_resigned) ->save(); break; case PhabricatorAuditActionConstants::ACCEPT: case PhabricatorAuditActionConstants::CONCERN: if ($new == PhabricatorAuditActionConstants::ACCEPT) { $new_status = $status_accepted; } else { $new_status = $status_concerned; } $requests = $object->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); // Figure out which requests the actor has authority over: these // are user requests where they are the auditor, and packages // and projects they are a member of. if ($actor_is_author) { // When modifying your own commits, you act only on behalf of // yourself, not your packages/projects -- the idea being that // you can't accept your own commits. $authority_phids = array($actor_phid); } else { $authority_phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser( $this->requireActor()); } $authority = array_select_keys( $requests, $authority_phids); if (!$authority) { // If the actor has no authority over any existing requests, // create a new request for them. $actor_request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($object->getPHID()) ->setAuditorPHID($actor_phid) ->setAuditStatus($new_status) ->save(); $requests[$actor_phid] = $actor_request; $object->attachAudits($requests); } else { // Otherwise, update the audit status of the existing requests. foreach ($authority as $request) { $request ->setAuditStatus($new_status) ->save(); } } break; } break; } } $requests = $object->getAudits(); $object->updateAuditStatus($requests); $object->save(); if ($import_status_flag) { $object->writeImportStatusFlag($import_status_flag); } return $xactions; } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $xactions = parent::expandTransaction($object, $xaction); switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $request = $this->createAuditRequestTransactionFromCommitMessage( $object); if ($request) { $xactions[] = $request; $this->setUnmentionablePHIDMap($request->getNewValue()); } break; default: break; } + + if (!$this->expandedDone) { + + switch ($xaction->getTransactionType()) { + case PhabricatorTransactions::TYPE_COMMENT: + case PhabricatorAuditActionConstants::ACTION: + $this->expandedDone = true; + + $actor_phid = $this->getActingAsPHID(); + $actor_is_author = ($object->getAuthorPHID() == $actor_phid); + if (!$actor_is_author) { + break; + } + + $state_map = array( + PhabricatorInlineCommentInterface::STATE_DRAFT => + PhabricatorInlineCommentInterface::STATE_DONE, + PhabricatorInlineCommentInterface::STATE_UNDRAFT => + PhabricatorInlineCommentInterface::STATE_UNDONE, + ); + + $inlines = id(new DiffusionDiffInlineCommentQuery()) + ->setViewer($this->getActor()) + ->withCommitPHIDs(array($object->getPHID())) + ->withFixedStates(array_keys($state_map)) + ->execute(); + + if (!$inlines) { + break; + } + + $old_value = mpull($inlines, 'getFixedState', 'getPHID'); + $new_value = array(); + foreach ($old_value as $key => $state) { + $new_value[$key] = $state_map[$state]; + } + + $xactions[] = id(new PhabricatorAuditTransaction()) + ->setTransactionType(PhabricatorAuditTransaction::TYPE_INLINEDONE) + ->setIgnoreOnNoEffect(true) + ->setOldValue($old_value) + ->setNewValue($new_value); + break; + } + } + return $xactions; } private function createAuditRequestTransactionFromCommitMessage( PhabricatorRepositoryCommit $commit) { $data = $commit->getCommitData(); $message = $data->getCommitMessage(); $matches = null; if (!preg_match('/^Auditors:\s*(.*)$/im', $message, $matches)) { return array(); } $phids = id(new PhabricatorObjectListQuery()) ->setViewer($this->getActor()) ->setAllowPartialResults(true) ->setAllowedTypes( array( PhabricatorPeopleUserPHIDType::TYPECONST, PhabricatorProjectProjectPHIDType::TYPECONST, )) ->setObjectList($matches[1]) ->execute(); if (!$phids) { return array(); } foreach ($phids as $phid) { $this->addAuditReason($phid, 'Requested by Author'); } return id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS) ->setNewValue(array_fuse($phids)); } protected function sortTransactions(array $xactions) { $xactions = parent::sortTransactions($xactions); $head = array(); $tail = array(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == PhabricatorAuditActionConstants::INLINE) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); foreach ($xactions as $xaction) { switch ($type) { case PhabricatorAuditActionConstants::ACTION: $error = $this->validateAuditAction( $object, $type, $xaction, $xaction->getNewValue()); if ($error) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), $error, $xaction); } break; } } return $errors; } private function validateAuditAction( PhabricatorLiskDAO $object, $type, PhabricatorAuditTransaction $xaction, $action) { $can_author_close_key = 'audit.can-author-close-audit'; $can_author_close = PhabricatorEnv::getEnvConfig($can_author_close_key); $actor_is_author = ($object->getAuthorPHID()) && ($object->getAuthorPHID() == $this->getActingAsPHID()); switch ($action) { case PhabricatorAuditActionConstants::CLOSE: if (!$actor_is_author) { return pht( 'You can not close this audit because you are not the author '. 'of the commit.'); } if (!$can_author_close) { return pht( 'You can not close this audit because "%s" is disabled in '. 'the Phabricator configuration.', $can_author_close_key); } break; } return null; } protected function supportsSearch() { return true; } protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, $blocks, PhutilMarkupEngine $engine) { // we are only really trying to find unmentionable phids here... // don't bother with this outside initial commit (i.e. create) // transaction $is_commit = false; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $is_commit = true; break; } } // "result" is always an array.... $result = array(); if (!$is_commit) { return $result; } $flat_blocks = array_mergev($blocks); $huge_block = implode("\n\n", $flat_blocks); $phid_map = array(); $phid_map[] = $this->getUnmentionablePHIDMap(); $monograms = array(); $task_refs = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($huge_block); foreach ($task_refs as $match) { foreach ($match['monograms'] as $monogram) { $monograms[] = $monogram; } } $rev_refs = id(new DifferentialCustomFieldDependsOnParser()) ->parseCorpus($huge_block); foreach ($rev_refs as $match) { foreach ($match['monograms'] as $monogram) { $monograms[] = $monogram; } } $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getActor()) ->withNames($monograms) ->execute(); $phid_map[] = mpull($objects, 'getPHID', 'getPHID'); $phid_map = array_mergev($phid_map); $this->setUnmentionablePHIDMap($phid_map); return $result; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { $reply_handler = PhabricatorEnv::newObjectFromConfig( 'metamta.diffusion.reply-handler'); $reply_handler->setMailReceiver($object); return $reply_handler; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { // For backward compatibility, use this legacy thread ID. return 'diffusion-audit-'.$object->getPHID(); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $identifier = $object->getCommitIdentifier(); $repository = $object->getRepository(); $monogram = $repository->getMonogram(); $summary = $object->getSummary(); $name = $repository->formatCommitName($identifier); $subject = "{$name}: {$summary}"; $thread_topic = "Commit {$monogram}{$identifier}"; $template = id(new PhabricatorMetaMTAMail()) ->setSubject($subject) ->addHeader('Thread-Topic', $thread_topic); $this->attachPatch( $template, $object); return $template; } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); if ($this->heraldEmailPHIDs) { $phids = $this->heraldEmailPHIDs; } if ($object->getAuthorPHID()) { $phids[] = $object->getAuthorPHID(); } $status_resigned = PhabricatorAuditStatusConstants::RESIGNED; foreach ($object->getAudits() as $audit) { if ($audit->getAuditStatus() != $status_resigned) { $phids[] = $audit->getAuditorPHID(); } } return $phids; } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $type_inline = PhabricatorAuditActionConstants::INLINE; $type_push = PhabricatorAuditTransaction::TYPE_COMMIT; $is_commit = false; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction; } if ($xaction->getTransactionType() == $type_push) { $is_commit = true; } } if ($inlines) { $body->addTextSection( pht('INLINE COMMENTS'), $this->renderInlineCommentsForMail($object, $inlines)); } if ($is_commit) { $data = $object->getCommitData(); $body->addTextSection(pht('AFFECTED FILES'), $this->affectedFiles); $this->inlinePatch( $body, $object); } // Reload the commit to pull commit data. $commit = id(new DiffusionCommitQuery()) ->setViewer($this->requireActor()) ->withIDs(array($object->getID())) ->needCommitData(true) ->executeOne(); $data = $commit->getCommitData(); $user_phids = array(); $author_phid = $commit->getAuthorPHID(); if ($author_phid) { $user_phids[$author_phid][] = pht('Author'); } $committer_phid = $data->getCommitDetail('committerPHID'); if ($committer_phid && ($committer_phid != $author_phid)) { $user_phids[$committer_phid][] = pht('Committer'); } // we loaded this in applyFinalEffects $audit_requests = $object->getAudits(); $auditor_phids = mpull($audit_requests, 'getAuditorPHID'); foreach ($auditor_phids as $auditor_phid) { $user_phids[$auditor_phid][] = pht('Auditor'); } // TODO: It would be nice to show pusher here too, but that information // is a little tricky to get at right now. if ($user_phids) { $handle_phids = array_keys($user_phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($handle_phids) ->execute(); $user_info = array(); foreach ($user_phids as $phid => $roles) { $user_info[] = pht( '%s (%s)', $handles[$phid]->getName(), implode(', ', $roles)); } $body->addTextSection( pht('USERS'), implode("\n", $user_info)); } $monogram = $object->getRepository()->formatCommitName( $object->getCommitIdentifier()); $body->addLinkSection( pht('COMMIT'), PhabricatorEnv::getProductionURI('/'.$monogram)); return $body; } private function attachPatch( PhabricatorMetaMTAMail $template, PhabricatorRepositoryCommit $commit) { if (!$this->getRawPatch()) { return; } $attach_key = 'metamta.diffusion.attach-patches'; $attach_patches = PhabricatorEnv::getEnvConfig($attach_key); if (!$attach_patches) { return; } $repository = $commit->getRepository(); $encoding = $repository->getDetail('encoding', 'UTF-8'); $raw_patch = $this->getRawPatch(); $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $template->addAttachment( new PhabricatorMetaMTAAttachment( $raw_patch, $commit_name.'.patch', 'text/x-patch; charset='.$encoding)); } private function inlinePatch( PhabricatorMetaMTAMailBody $body, PhabricatorRepositoryCommit $commit) { if (!$this->getRawPatch()) { return; } $inline_key = 'metamta.diffusion.inline-patches'; $inline_patches = PhabricatorEnv::getEnvConfig($inline_key); if (!$inline_patches) { return; } $repository = $commit->getRepository(); $raw_patch = $this->getRawPatch(); $result = null; $len = substr_count($raw_patch, "\n"); if ($len <= $inline_patches) { // We send email as utf8, so we need to convert the text to utf8 if // we can. $encoding = $repository->getDetail('encoding', 'UTF-8'); if ($encoding) { $raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding); } $result = phutil_utf8ize($raw_patch); } if ($result) { $result = "PATCH\n\n{$result}\n"; } $body->addRawSection($result); } private function renderInlineCommentsForMail( PhabricatorLiskDAO $object, array $inline_xactions) { $inlines = mpull($inline_xactions, 'getComment'); $block = array(); $path_map = id(new DiffusionPathQuery()) ->withPathIDs(mpull($inlines, 'getPathID')) ->execute(); $path_map = ipull($path_map, 'path', 'id'); foreach ($inlines as $inline) { $path = idx($path_map, $inline->getPathID()); if ($path === null) { continue; } $start = $inline->getLineNumber(); $len = $inline->getLineLength(); if ($len) { $range = $start.'-'.($start + $len); } else { $range = $start; } $content = $inline->getContent(); $block[] = "{$path}:{$range} {$content}"; } return implode("\n", $block); } public function getMailTagsMap() { return array( PhabricatorAuditTransaction::MAILTAG_COMMIT => pht('A commit is created.'), PhabricatorAuditTransaction::MAILTAG_ACTION_CONCERN => pht('A commit has a concerned raised against it.'), PhabricatorAuditTransaction::MAILTAG_ACTION_ACCEPT => pht('A commit is accepted.'), PhabricatorAuditTransaction::MAILTAG_ACTION_RESIGN => pht('A commit has an auditor resign.'), PhabricatorAuditTransaction::MAILTAG_ACTION_CLOSE => pht('A commit is closed.'), PhabricatorAuditTransaction::MAILTAG_ADD_AUDITORS => pht('A commit has auditors added.'), PhabricatorAuditTransaction::MAILTAG_ADD_CCS => pht("A commit's subscribers change."), PhabricatorAuditTransaction::MAILTAG_PROJECTS => pht("A commit's projects change."), PhabricatorAuditTransaction::MAILTAG_COMMENT => pht('Someone comments on a commit.'), PhabricatorAuditTransaction::MAILTAG_OTHER => pht('Other commit activity not listed above occurs.'), ); } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $repository = $object->getRepository(); if (!$repository->shouldPublish()) { return false; } return true; default: break; } } return parent::shouldApplyHeraldRules($object, $xactions); } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new HeraldCommitAdapter()) ->setCommit($object); } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { $xactions = array(); $audit_phids = $adapter->getAuditMap(); foreach ($audit_phids as $phid => $rule_ids) { foreach ($rule_ids as $rule_id) { $this->addAuditReason( $phid, pht( '%s Triggered Audit', "H{$rule_id}")); } } if ($audit_phids) { $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS) ->setNewValue(array_fuse(array_keys($audit_phids))) ->setMetadataValue( 'auditStatus', PhabricatorAuditStatusConstants::AUDIT_REQUIRED) ->setMetadataValue( 'auditReasonMap', $this->auditReasonMap); } $cc_phids = $adapter->getAddCCMap(); $add_ccs = array('+' => array()); foreach ($cc_phids as $phid => $rule_ids) { $add_ccs['+'][$phid] = $phid; } $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue($add_ccs); $this->heraldEmailPHIDs = $adapter->getEmailPHIDs(); HarbormasterBuildable::applyBuildPlans( $object->getPHID(), $object->getRepository()->getPHID(), $adapter->getBuildPlans()); $limit = self::MAX_FILES_SHOWN_IN_EMAIL; $files = $adapter->loadAffectedPaths(); sort($files); if (count($files) > $limit) { array_splice($files, $limit); $files[] = pht( '(This commit affected more than %d files. Only %d are shown here '. 'and additional ones are truncated.)', $limit, $limit); } $this->affectedFiles = implode("\n", $files); return $xactions; } private function isCommitMostlyImported(PhabricatorLiskDAO $object) { $has_message = PhabricatorRepositoryCommit::IMPORTED_MESSAGE; $has_changes = PhabricatorRepositoryCommit::IMPORTED_CHANGE; // Don't publish feed stories or email about events which occur during // import. In particular, this affects tasks being attached when they are // closed by "Fixes Txxxx" in a commit message. See T5851. $mask = ($has_message | $has_changes); return $object->isPartiallyImported($mask); } private function shouldPublishRepositoryActivity( PhabricatorLiskDAO $object, array $xactions) { // not every code path loads the repository so tread carefully // TODO: They should, and then we should simplify this. if ($object->getRepository($assert_attached = false)) { $repository = $object->getRepository(); if (!$repository->shouldPublish()) { return false; } } return $this->isCommitMostlyImported($object); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } protected function shouldEnableMentions( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } } diff --git a/src/applications/audit/storage/PhabricatorAuditTransaction.php b/src/applications/audit/storage/PhabricatorAuditTransaction.php index 4d21135bdc..52be00788c 100644 --- a/src/applications/audit/storage/PhabricatorAuditTransaction.php +++ b/src/applications/audit/storage/PhabricatorAuditTransaction.php @@ -1,472 +1,513 @@ getTransactionType()) { case self::TYPE_COMMIT: $data = $this->getNewValue(); $blocks[] = $data['description']; break; } return $blocks; } public function getRequiredHandlePHIDs() { $phids = parent::getRequiredHandlePHIDs(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_COMMIT: $phids[] = $this->getObjectPHID(); $data = $this->getNewValue(); if ($data['authorPHID']) { $phids[] = $data['authorPHID']; } if ($data['committerPHID']) { $phids[] = $data['committerPHID']; } break; case PhabricatorAuditActionConstants::ADD_CCS: case PhabricatorAuditActionConstants::ADD_AUDITORS: $old = $this->getOldValue(); $new = $this->getNewValue(); if (!is_array($old)) { $old = array(); } if (!is_array($new)) { $new = array(); } foreach (array_keys($old + $new) as $phid) { $phids[] = $phid; } break; } return $phids; } public function getActionName() { switch ($this->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: switch ($this->getNewValue()) { case PhabricatorAuditActionConstants::CONCERN: return pht('Raised Concern'); case PhabricatorAuditActionConstants::ACCEPT: return pht('Accepted'); case PhabricatorAuditActionConstants::RESIGN: return pht('Resigned'); case PhabricatorAuditActionConstants::CLOSE: return pht('Closed'); } break; case PhabricatorAuditActionConstants::ADD_AUDITORS: return pht('Added Auditors'); case self::TYPE_COMMIT: return pht('Committed'); } return parent::getActionName(); } public function getColor() { $type = $this->getTransactionType(); switch ($type) { case PhabricatorAuditActionConstants::ACTION: switch ($this->getNewValue()) { case PhabricatorAuditActionConstants::CONCERN: return 'red'; case PhabricatorAuditActionConstants::ACCEPT: return 'green'; } } return parent::getColor(); } public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); $author_handle = $this->renderHandleLink($this->getAuthorPHID()); $type = $this->getTransactionType(); switch ($type) { case PhabricatorAuditActionConstants::ADD_CCS: case PhabricatorAuditActionConstants::ADD_AUDITORS: if (!is_array($old)) { $old = array(); } if (!is_array($new)) { $new = array(); } $add = array_keys(array_diff_key($new, $old)); $rem = array_keys(array_diff_key($old, $new)); break; } switch ($type) { case self::TYPE_COMMIT: $author = null; if ($new['authorPHID']) { $author = $this->renderHandleLink($new['authorPHID']); } else { $author = $new['authorName']; } $committer = null; if ($new['committerPHID']) { $committer = $this->renderHandleLink($new['committerPHID']); } else if ($new['committerName']) { $committer = $new['committerName']; } $commit = $this->renderHandleLink($this->getObjectPHID()); if (!$committer) { $committer = $author; $author = null; } if ($author) { $title = pht( '%s committed %s (authored by %s).', $committer, $commit, $author); } else { $title = pht( '%s committed %s.', $committer, $commit); } return $title; + case self::TYPE_INLINEDONE: + $done = 0; + $undone = 0; + foreach ($new as $phid => $state) { + if ($state == PhabricatorInlineCommentInterface::STATE_DONE) { + $done++; + } else { + $undone++; + } + } + if ($done && $undone) { + return pht( + '%s marked %s inline comment(s) as done and %s inline comment(s) '. + 'as not done.', + $author_handle, + new PhutilNumber($done), + new PhutilNumber($undone)); + } else if ($done) { + return pht( + '%s marked %s inline comment(s) as done.', + $author_handle, + new PhutilNumber($done)); + } else { + return pht( + '%s marked %s inline comment(s) as not done.', + $author_handle, + new PhutilNumber($undone)); + } + break; + case PhabricatorAuditActionConstants::INLINE: return pht( '%s added inline comments.', $author_handle); case PhabricatorAuditActionConstants::ADD_CCS: if ($add && $rem) { return pht( '%s edited subscribers; added: %s, removed: %s.', $author_handle, $this->renderHandleList($add), $this->renderHandleList($rem)); } else if ($add) { return pht( '%s added subscribers: %s.', $author_handle, $this->renderHandleList($add)); } else if ($rem) { return pht( '%s removed subscribers: %s.', $author_handle, $this->renderHandleList($rem)); } else { return pht( '%s added subscribers...', $author_handle); } case PhabricatorAuditActionConstants::ADD_AUDITORS: if ($add && $rem) { return pht( '%s edited auditors; added: %s, removed: %s.', $author_handle, $this->renderHandleList($add), $this->renderHandleList($rem)); } else if ($add) { return pht( '%s added auditors: %s.', $author_handle, $this->renderHandleList($add)); } else if ($rem) { return pht( '%s removed auditors: %s.', $author_handle, $this->renderHandleList($rem)); } else { return pht( '%s added auditors...', $author_handle); } case PhabricatorAuditActionConstants::ACTION: switch ($new) { case PhabricatorAuditActionConstants::ACCEPT: return pht( '%s accepted this commit.', $author_handle); case PhabricatorAuditActionConstants::CONCERN: return pht( '%s raised a concern with this commit.', $author_handle); case PhabricatorAuditActionConstants::RESIGN: return pht( '%s resigned from this audit.', $author_handle); case PhabricatorAuditActionConstants::CLOSE: return pht( '%s closed this audit.', $author_handle); } } return parent::getTitle(); } public function getTitleForFeed() { $old = $this->getOldValue(); $new = $this->getNewValue(); $author_handle = $this->renderHandleLink($this->getAuthorPHID()); $object_handle = $this->renderHandleLink($this->getObjectPHID()); $type = $this->getTransactionType(); switch ($type) { case PhabricatorAuditActionConstants::ADD_CCS: case PhabricatorAuditActionConstants::ADD_AUDITORS: if (!is_array($old)) { $old = array(); } if (!is_array($new)) { $new = array(); } $add = array_keys(array_diff_key($new, $old)); $rem = array_keys(array_diff_key($old, $new)); break; } switch ($type) { case self::TYPE_COMMIT: $author = null; if ($new['authorPHID']) { $author = $this->renderHandleLink($new['authorPHID']); } else { $author = $new['authorName']; } $committer = null; if ($new['committerPHID']) { $committer = $this->renderHandleLink($new['committerPHID']); } else if ($new['committerName']) { $committer = $new['committerName']; } if (!$committer) { $committer = $author; $author = null; } if ($author) { $title = pht( '%s committed %s (authored by %s).', $committer, $object_handle, $author); } else { $title = pht( '%s committed %s.', $committer, $object_handle); } return $title; case PhabricatorAuditActionConstants::INLINE: return pht( '%s added inline comments to %s.', $author_handle, $object_handle); case PhabricatorAuditActionConstants::ADD_AUDITORS: if ($add && $rem) { return pht( '%s edited auditors for %s; added: %s, removed: %s.', $author_handle, $object_handle, $this->renderHandleList($add), $this->renderHandleList($rem)); } else if ($add) { return pht( '%s added auditors to %s: %s.', $author_handle, $object_handle, $this->renderHandleList($add)); } else if ($rem) { return pht( '%s removed auditors from %s: %s.', $author_handle, $object_handle, $this->renderHandleList($rem)); } else { return pht( '%s added auditors to %s...', $author_handle, $object_handle); } case PhabricatorAuditActionConstants::ACTION: switch ($new) { case PhabricatorAuditActionConstants::ACCEPT: return pht( '%s accepted %s.', $author_handle, $object_handle); case PhabricatorAuditActionConstants::CONCERN: return pht( '%s raised a concern with %s.', $author_handle, $object_handle); case PhabricatorAuditActionConstants::RESIGN: return pht( '%s resigned from auditing %s.', $author_handle, $object_handle); case PhabricatorAuditActionConstants::CLOSE: return pht( '%s closed the audit of %s.', $author_handle, $object_handle); } } return parent::getTitleForFeed(); } public function getBodyForFeed(PhabricatorFeedStory $story) { switch ($this->getTransactionType()) { case self::TYPE_COMMIT: $data = $this->getNewValue(); return $story->renderSummary($data['summary']); } return parent::getBodyForFeed($story); } + public function shouldGenerateOldValue() { + switch ($this->getTransactionType()) { + case self::TYPE_INLINEDONE: + return false; + } + + return parent::shouldGenerateOldValue(); + } + + // TODO: These two mail methods can likely be abstracted by introducing a // formal concept of "inline comment" transactions. public function shouldHideForMail(array $xactions) { $type_inline = PhabricatorAuditActionConstants::INLINE; switch ($this->getTransactionType()) { case $type_inline: foreach ($xactions as $xaction) { if ($xaction->getTransactionType() != $type_inline) { return true; } } return ($this !== head($xactions)); } return parent::shouldHideForMail($xactions); } public function getBodyForMail() { switch ($this->getTransactionType()) { case PhabricatorAuditActionConstants::INLINE: return null; case self::TYPE_COMMIT: $data = $this->getNewValue(); return $data['description']; } return parent::getBodyForMail(); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: switch ($this->getNewValue()) { case PhabricatorAuditActionConstants::CONCERN: $tags[] = self::MAILTAG_ACTION_CONCERN; break; case PhabricatorAuditActionConstants::ACCEPT: $tags[] = self::MAILTAG_ACTION_ACCEPT; break; case PhabricatorAuditActionConstants::RESIGN: $tags[] = self::MAILTAG_ACTION_RESIGN; break; case PhabricatorAuditActionConstants::CLOSE: $tags[] = self::MAILTAG_ACTION_CLOSE; break; } break; case PhabricatorAuditActionConstants::ADD_AUDITORS: $tags[] = self::MAILTAG_ADD_AUDITORS; break; case PhabricatorAuditActionConstants::ADD_CCS: $tags[] = self::MAILTAG_ADD_CCS; break; case PhabricatorAuditActionConstants::INLINE: case PhabricatorTransactions::TYPE_COMMENT: $tags[] = self::MAILTAG_COMMENT; break; case self::TYPE_COMMIT: $tags[] = self::MAILTAG_COMMIT; break; case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST: $tags[] = self::MAILTAG_PROJECTS; break; case PhabricatorObjectHasSubscriberEdgeType::EDGECONST: $tags[] = self::MAILTAG_ADD_CCS; break; default: $tags[] = self::MAILTAG_OTHER; break; } break; default: $tags[] = self::MAILTAG_OTHER; break; } return $tags; } } diff --git a/src/applications/diffusion/controller/DiffusionInlineCommentController.php b/src/applications/diffusion/controller/DiffusionInlineCommentController.php index 9cdb3fc349..f3e19e42d9 100644 --- a/src/applications/diffusion/controller/DiffusionInlineCommentController.php +++ b/src/applications/diffusion/controller/DiffusionInlineCommentController.php @@ -1,116 +1,116 @@ commitPHID = $data['phid']; } protected function createComment() { // Verify commit and path correspond to actual objects. $commit_phid = $this->commitPHID; $path_id = $this->getChangesetID(); $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( 'phid = %s', $commit_phid); if (!$commit) { throw new Exception('Invalid commit ID!'); } // TODO: Write a real PathQuery object? $path = queryfx_one( id(new PhabricatorRepository())->establishConnection('r'), 'SELECT path FROM %T WHERE id = %d', PhabricatorRepository::TABLE_PATH, $path_id); if (!$path) { throw new Exception('Invalid path ID!'); } return id(new PhabricatorAuditInlineComment()) ->setCommitPHID($commit_phid) ->setPathID($path_id); } protected function loadComment($id) { return PhabricatorAuditInlineComment::loadID($id); } protected function loadCommentByPHID($phid) { return PhabricatorAuditInlineComment::loadPHID($phid); } protected function loadCommentForEdit($id) { $request = $this->getRequest(); $user = $request->getUser(); $inline = $this->loadComment($id); if (!$this->canEditInlineComment($user, $inline)) { throw new Exception('That comment is not editable!'); } return $inline; } protected function loadCommentForDone($id) { $request = $this->getRequest(); $viewer = $request->getUser(); $inline = $this->loadComment($id); if (!$inline) { throw new Exception(pht('Failed to load comment "%d".', $id)); } $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withPHIDs(array($inline->getCommitPHID())) - ->exeucteOne(); + ->executeOne(); if (!$commit) { throw new Exception(pht('Failed to load commit.')); } if ((!$commit->getAuthorPHID()) || ($commit->getAuthorPHID() != $viewer->getPHID())) { throw new Exception(pht('You can not mark this comment as complete.')); } return $inline; } private function canEditInlineComment( PhabricatorUser $user, PhabricatorAuditInlineComment $inline) { // Only the author may edit a comment. if ($inline->getAuthorPHID() != $user->getPHID()) { return false; } // Saved comments may not be edited. if ($inline->getAuditCommentID()) { return false; } // Inline must be attached to the active revision. if ($inline->getCommitPHID() != $this->commitPHID) { return false; } return true; } protected function deleteComment(PhabricatorInlineCommentInterface $inline) { return $inline->delete(); } protected function saveComment(PhabricatorInlineCommentInterface $inline) { return $inline->save(); } }