diff --git a/resources/sql/autopatches/20141119.differential.diff.policy.sql b/resources/sql/autopatches/20141119.differential.diff.policy.sql new file mode 100644 index 0000000000..b6c19c0dea --- /dev/null +++ b/resources/sql/autopatches/20141119.differential.diff.policy.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_diff + ADD viewPolicy VARBINARY(64) NOT NULL; + +UPDATE {$NAMESPACE}_differential.differential_diff + SET viewPolicy = 'users' WHERE viewPolicy = ''; diff --git a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php index 41165bf23e..9d9f47315d 100644 --- a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php +++ b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php @@ -1,64 +1,66 @@ buildChangesetParser($type, $data, $file); $actual = $parser->render(null, null, array()); $expect = Filesystem::readFile($dir.$file.'.'.$type.'.expect'); $this->assertEqual($expect, (string)$actual, $file.'.'.$type); } } } private function buildChangesetParser($type, $data, $file) { $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($data); - $diff = DifferentialDiff::newFromRawChanges($changes); + $diff = DifferentialDiff::newFromRawChanges( + PhabricatorUser::getOmnipotentUser(), + $changes); if (count($diff->getChangesets()) !== 1) { throw new Exception("Expected one changeset: {$file}"); } $changeset = head($diff->getChangesets()); $engine = new PhabricatorMarkupEngine(); $engine->setViewer(new PhabricatorUser()); $cparser = new DifferentialChangesetParser(); $cparser->setDisableCache(true); $cparser->setChangeset($changeset); $cparser->setMarkupEngine($engine); if ($type == 'one') { $cparser->setRenderer(new DifferentialChangesetOneUpTestRenderer()); } else if ($type == 'two') { $cparser->setRenderer(new DifferentialChangesetTwoUpTestRenderer()); } else { throw new Exception("Unknown renderer type '{$type}'!"); } return $cparser; } } diff --git a/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php index 3ea3c1768f..26b1c04ddd 100644 --- a/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php @@ -1,186 +1,186 @@ formatStringConstants( array( 'svn', 'git', 'hg', )); $status_const = $this->formatStringConstants( array( 'none', 'skip', 'okay', 'warn', 'fail', 'postponed', )); return array( 'changes' => 'required list', 'sourceMachine' => 'required string', 'sourcePath' => 'required string', 'branch' => 'required string', 'bookmark' => 'optional string', 'sourceControlSystem' => 'required '.$vcs_const, 'sourceControlPath' => 'required string', 'sourceControlBaseRevision' => 'required string', 'creationMethod' => 'optional string', 'arcanistProject' => 'optional string', 'lintStatus' => 'required '.$status_const, 'unitStatus' => 'required '.$status_const, 'repositoryPHID' => 'optional phid', 'parentRevisionID' => 'deprecated', 'authorPHID' => 'deprecated', 'repositoryUUID' => 'deprecated', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $viewer = $request->getUser(); $change_data = $request->getValue('changes'); $changes = array(); foreach ($change_data as $dict) { $changes[] = ArcanistDiffChange::newFromDictionary($dict); } - $diff = DifferentialDiff::newFromRawChanges($changes); + $diff = DifferentialDiff::newFromRawChanges($viewer, $changes); // TODO: Remove repository UUID eventually; for now continue writing // the UUID. Note that we'll overwrite it below if we identify a // repository, and `arc` no longer sends it. This stuff is retained for // backward compatibility. $repository_uuid = $request->getValue('repositoryUUID'); $repository_phid = $request->getValue('repositoryPHID'); if ($repository_phid) { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array($repository_phid)) ->executeOne(); if ($repository) { $repository_phid = $repository->getPHID(); $repository_uuid = $repository->getUUID(); } } $project_name = $request->getValue('arcanistProject'); $project_phid = null; if ($project_name) { $arcanist_project = id(new PhabricatorRepositoryArcanistProject()) ->loadOneWhere( 'name = %s', $project_name); if (!$arcanist_project) { $arcanist_project = new PhabricatorRepositoryArcanistProject(); $arcanist_project->setName($project_name); $arcanist_project->save(); } $project_phid = $arcanist_project->getPHID(); } switch ($request->getValue('lintStatus')) { case 'skip': $lint_status = DifferentialLintStatus::LINT_SKIP; break; case 'okay': $lint_status = DifferentialLintStatus::LINT_OKAY; break; case 'warn': $lint_status = DifferentialLintStatus::LINT_WARN; break; case 'fail': $lint_status = DifferentialLintStatus::LINT_FAIL; break; case 'postponed': $lint_status = DifferentialLintStatus::LINT_POSTPONED; break; case 'none': default: $lint_status = DifferentialLintStatus::LINT_NONE; break; } switch ($request->getValue('unitStatus')) { case 'skip': $unit_status = DifferentialUnitStatus::UNIT_SKIP; break; case 'okay': $unit_status = DifferentialUnitStatus::UNIT_OKAY; break; case 'warn': $unit_status = DifferentialUnitStatus::UNIT_WARN; break; case 'fail': $unit_status = DifferentialUnitStatus::UNIT_FAIL; break; case 'postponed': $unit_status = DifferentialUnitStatus::UNIT_POSTPONED; break; case 'none': default: $unit_status = DifferentialUnitStatus::UNIT_NONE; break; } $diff_data_dict = array( 'sourcePath' => $request->getValue('sourcePath'), 'sourceMachine' => $request->getValue('sourceMachine'), 'branch' => $request->getValue('branch'), 'creationMethod' => $request->getValue('creationMethod'), 'authorPHID' => $viewer->getPHID(), 'bookmark' => $request->getValue('bookmark'), 'repositoryUUID' => $repository_uuid, 'repositoryPHID' => $repository_phid, 'sourceControlSystem' => $request->getValue('sourceControlSystem'), 'sourceControlPath' => $request->getValue('sourceControlPath'), 'sourceControlBaseRevision' => $request->getValue('sourceControlBaseRevision'), 'arcanistProjectPHID' => $project_phid, 'lintStatus' => $lint_status, 'unitStatus' => $unit_status,); $xactions = array(id(new DifferentialTransaction()) ->setTransactionType(DifferentialDiffTransaction::TYPE_DIFF_CREATE) ->setNewValue($diff_data_dict),); id(new DifferentialDiffEditor()) ->setActor($viewer) ->setContentSourceFromConduitRequest($request) ->applyTransactions($diff, $xactions); $path = '/differential/diff/'.$diff->getID().'/'; $uri = PhabricatorEnv::getURI($path); return array( 'diffid' => $diff->getID(), 'uri' => $uri, ); } } diff --git a/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php index 68531ea6bc..29638b5396 100644 --- a/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php @@ -1,70 +1,77 @@ 'required string', 'repositoryPHID' => 'optional string', + 'viewPolicy' => 'optional string', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $viewer = $request->getUser(); $raw_diff = $request->getValue('diff'); $repository_phid = $request->getValue('repositoryPHID'); if ($repository_phid) { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array($repository_phid)) ->executeOne(); if (!$repository) { throw new Exception( pht('No such repository "%s"!', $repository_phid)); } } $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw_diff); - $diff = DifferentialDiff::newFromRawChanges($changes); + $diff = DifferentialDiff::newFromRawChanges($viewer, $changes); $diff_data_dict = array( 'creationMethod' => 'web', 'authorPHID' => $viewer->getPHID(), 'repositoryPHID' => $repository_phid, 'lintStatus' => DifferentialLintStatus::LINT_SKIP, 'unitStatus' => DifferentialUnitStatus::UNIT_SKIP,); $xactions = array(id(new DifferentialTransaction()) ->setTransactionType(DifferentialDiffTransaction::TYPE_DIFF_CREATE) ->setNewValue($diff_data_dict),); + if ($request->getValue('viewPolicy')) { + $xactions[] = id(new DifferentialTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($request->getValue('viewPolicy')); + } + id(new DifferentialDiffEditor()) ->setActor($viewer) ->setContentSourceFromConduitRequest($request) ->setLookupRepository(false) // respect user choice ->applyTransactions($diff, $xactions); return $this->buildDiffInfoDictionary($diff); } } diff --git a/src/applications/differential/controller/DifferentialDiffCreateController.php b/src/applications/differential/controller/DifferentialDiffCreateController.php index c092597fbb..9c3698e3f2 100644 --- a/src/applications/differential/controller/DifferentialDiffCreateController.php +++ b/src/applications/differential/controller/DifferentialDiffCreateController.php @@ -1,127 +1,143 @@ getRequest(); + $viewer = $request->getUser(); $diff = null; + // This object is just for policy stuff + $diff_object = DifferentialDiff::initializeNewDiff($viewer); $repository_phid = null; $repository_value = array(); $errors = array(); $e_diff = null; $e_file = null; $validation_exception = null; if ($request->isFormPost()) { $repository_tokenizer = $request->getArr( id(new DifferentialRepositoryField())->getFieldKey()); if ($repository_tokenizer) { $repository_phid = reset($repository_tokenizer); } if ($request->getFileExists('diff-file')) { $diff = PhabricatorFile::readUploadedFileData($_FILES['diff-file']); } else { $diff = $request->getStr('diff'); } if (!strlen($diff)) { $errors[] = pht( 'You can not create an empty diff. Copy/paste a diff, or upload a '. 'diff file.'); $e_diff = pht('Required'); $e_file = pht('Required'); } if (!$errors) { try { $call = new ConduitCall( 'differential.createrawdiff', array( 'diff' => $diff, - 'repositoryPHID' => $repository_phid,)); - $call->setUser($request->getUser()); + 'repositoryPHID' => $repository_phid, + 'viewPolicy' => $request->getStr('viewPolicy'),)); + $call->setUser($viewer); $result = $call->execute(); $path = id(new PhutilURI($result['uri']))->getPath(); return id(new AphrontRedirectResponse())->setURI($path); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } } $form = new AphrontFormView(); $arcanist_href = PhabricatorEnv::getDoclink('Arcanist User Guide'); $arcanist_link = phutil_tag( 'a', array( 'href' => $arcanist_href, 'target' => '_blank', ), 'Arcanist'); $cancel_uri = $this->getApplicationURI(); if ($repository_phid) { $repository_value = $this->loadViewerHandles(array($repository_phid)); } + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($diff_object) + ->execute(); + $form ->setAction('/differential/diff/create/') ->setEncType('multipart/form-data') - ->setUser($request->getUser()) + ->setUser($viewer) ->appendInstructions( pht( 'The best way to create a Differential diff is by using %s, but you '. 'can also just paste a diff (for example, from %s, %s or %s) into '. 'this box, or upload a diff file.', $arcanist_link, phutil_tag('tt', array(), 'svn diff'), phutil_tag('tt', array(), 'git diff'), phutil_tag('tt', array(), 'hg diff --git'))) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Raw Diff')) ->setName('diff') ->setValue($diff) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setError($e_diff)) ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('Raw Diff From File')) ->setName('diff-file') ->setError($e_file)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setName(id(new DifferentialRepositoryField())->getFieldKey()) ->setLabel(pht('Repository')) ->setDatasource(new DiffusionRepositoryDatasource()) ->setValue($repository_value) ->setLimit(1)) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setUser($viewer) + ->setName('viewPolicy') + ->setPolicyObject($diff_object) + ->setPolicies($policies) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue(pht('Create Diff'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Diff')) ->setValidationException($validation_exception) ->setForm($form) ->setFormErrors($errors); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Create Diff')); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Create Diff'), )); } } diff --git a/src/applications/differential/controller/DifferentialRevisionEditController.php b/src/applications/differential/controller/DifferentialRevisionEditController.php index 14bcdf0476..ea26693454 100644 --- a/src/applications/differential/controller/DifferentialRevisionEditController.php +++ b/src/applications/differential/controller/DifferentialRevisionEditController.php @@ -1,203 +1,210 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if (!$this->id) { $this->id = $request->getInt('revisionID'); } if ($this->id) { $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->needRelationships(true) ->needReviewerStatus(true) ->needActiveDiffs(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$revision) { return new Aphront404Response(); } } else { $revision = DifferentialRevision::initializeNewRevision($viewer); $revision->attachReviewerStatus(array()); } $diff_id = $request->getInt('diffID'); if ($diff_id) { $diff = id(new DifferentialDiffQuery()) ->setViewer($viewer) ->withIDs(array($diff_id)) ->executeOne(); if (!$diff) { return new Aphront404Response(); } if ($diff->getRevisionID()) { // TODO: Redirect? throw new Exception('This diff is already attached to a revision!'); } } else { $diff = null; } if (!$diff) { if (!$revision->getID()) { throw new Exception( pht('You can not create a new revision without a diff!')); } } else { // TODO: It would be nice to show the diff being attached in the UI. } $field_list = PhabricatorCustomField::getObjectFields( $revision, PhabricatorCustomField::ROLE_EDIT); $field_list ->setViewer($viewer) ->readFieldsFromStorage($revision); if ($request->getStr('viaDiffView') && $diff) { $repo_key = id(new DifferentialRepositoryField())->getFieldKey(); $repository_field = idx( $field_list->getFields(), $repo_key); if ($repository_field) { $repository_field->setValue($request->getStr($repo_key)); } + $view_policy_key = id(new DifferentialViewPolicyField())->getFieldKey(); + $view_policy_field = idx( + $field_list->getFields(), + $view_policy_key); + if ($view_policy_field) { + $view_policy_field->setValue($diff->getViewPolicy()); + } } $validation_exception = null; if ($request->isFormPost() && !$request->getStr('viaDiffView')) { $editor = id(new DifferentialTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); $xactions = $field_list->buildFieldTransactionsFromRequest( new DifferentialTransaction(), $request); if ($diff) { $repository_phid = null; $repository_tokenizer = $request->getArr( id(new DifferentialRepositoryField())->getFieldKey()); if ($repository_tokenizer) { $repository_phid = reset($repository_tokenizer); } $xactions[] = id(new DifferentialTransaction()) ->setTransactionType(DifferentialTransaction::TYPE_UPDATE) ->setNewValue($diff->getPHID()); $editor->setRepositoryPHIDOverride($repository_phid); } $comments = $request->getStr('comments'); if (strlen($comments)) { $xactions[] = id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new DifferentialTransactionComment()) ->setContent($comments)); } try { $editor->applyTransactions($revision, $xactions); $revision_uri = '/D'.$revision->getID(); return id(new AphrontRedirectResponse())->setURI($revision_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } $form = new AphrontFormView(); $form->setUser($request->getUser()); if ($diff) { $form->addHiddenInput('diffID', $diff->getID()); } if ($revision->getID()) { $form->setAction('/differential/revision/edit/'.$revision->getID().'/'); } else { $form->setAction('/differential/revision/edit/'); } if ($diff && $revision->getID()) { $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Comments')) ->setName('comments') ->setCaption(pht("Explain what's new in this diff.")) ->setValue($request->getStr('comments'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save'))) ->appendChild( id(new AphrontFormDividerControl())); } $field_list->appendFieldsToForm($form); $submit = id(new AphrontFormSubmitControl()) ->setValue('Save'); if ($diff) { $submit->addCancelButton('/differential/diff/'.$diff->getID().'/'); } else { $submit->addCancelButton('/D'.$revision->getID()); } $form->appendChild($submit); $crumbs = $this->buildApplicationCrumbs(); if ($revision->getID()) { if ($diff) { $title = pht('Update Differential Revision'); $crumbs->addTextCrumb( 'D'.$revision->getID(), '/differential/diff/'.$diff->getID().'/'); } else { $title = pht('Edit Differential Revision'); $crumbs->addTextCrumb( 'D'.$revision->getID(), '/D'.$revision->getID()); } } else { $title = pht('Create New Differential Revision'); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setValidationException($validation_exception) ->setForm($form); $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/differential/editor/DifferentialDiffEditor.php b/src/applications/differential/editor/DifferentialDiffEditor.php index c2513dab7a..69125da1e1 100644 --- a/src/applications/differential/editor/DifferentialDiffEditor.php +++ b/src/applications/differential/editor/DifferentialDiffEditor.php @@ -1,242 +1,247 @@ lookupRepository = $bool; return $this; } public function getEditorApplicationClass() { return 'PhabricatorDifferentialApplication'; } public function getEditorObjectsDescription() { return pht('Differential Diffs'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); + $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = DifferentialDiffTransaction::TYPE_DIFF_CREATE; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialDiffTransaction::TYPE_DIFF_CREATE: return null; } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialDiffTransaction::TYPE_DIFF_CREATE: $this->diffDataDict = $xaction->getNewValue(); return true; } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialDiffTransaction::TYPE_DIFF_CREATE: $dict = $this->diffDataDict; $this->updateDiffFromDict($object, $dict); return; + case PhabricatorTransactions::TYPE_VIEW_POLICY: + $object->setViewPolicy($xaction->getNewValue()); + return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialDiffTransaction::TYPE_DIFF_CREATE: + case PhabricatorTransactions::TYPE_VIEW_POLICY: return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // If we didn't get an explicit `repositoryPHID` (which means the client // is old, or couldn't figure out which repository the working copy // belongs to), apply heuristics to try to figure it out. if ($this->lookupRepository && !$object->getRepositoryPHID()) { $repository = id(new DifferentialRepositoryLookup()) ->setDiff($object) ->setViewer($this->getActor()) ->lookupRepository(); if ($repository) { $object->setRepositoryPHID($repository->getPHID()); $object->setRepositoryUUID($repository->getUUID()); $object->save(); } } return $xactions; } /** * We run Herald as part of transaction validation because Herald can * block diff creation for Differential diffs. Its important to do this * separately so no Herald logs are saved; these logs could expose * information the Herald rules are inteneded to block. */ protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); foreach ($xactions as $xaction) { switch ($type) { case DifferentialDiffTransaction::TYPE_DIFF_CREATE: $diff = clone $object; $diff = $this->updateDiffFromDict($diff, $xaction->getNewValue()); $adapter = $this->buildHeraldAdapter($diff, $xactions); $adapter->setContentSource($this->getContentSource()); $adapter->setIsNewObject($this->getIsNewObject()); $engine = new HeraldEngine(); $rules = $engine->loadRulesForAdapter($adapter); $rules = mpull($rules, null, 'getID'); $effects = $engine->applyRules($rules, $adapter); $blocking_effect = null; foreach ($effects as $effect) { if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) { $blocking_effect = $effect; break; } } if ($blocking_effect) { $rule = idx($rules, $effect->getRuleID()); if ($rule && strlen($rule->getName())) { $rule_name = $rule->getName(); } else { $rule_name = pht('Unnamed Herald Rule'); } $message = $effect->getTarget(); if (!strlen($message)) { $message = pht('(None.)'); } $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Rejected by Herald'), pht( "Creation of this diff was rejected by Herald rule %s.\n". " Rule: %s\n". "Reason: %s", 'H'.$effect->getRuleID(), $rule_name, $message)); } break; } } return $errors; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return false; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return false; } protected function supportsSearch() { return false; } /* -( Herald Integration )------------------------------------------------- */ /** * See @{method:validateTransaction}. The only Herald action is to block * the creation of Diffs. We thus have to be careful not to save any * data and do this validation very early. */ protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return false; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { $adapter = id(new HeraldDifferentialDiffAdapter()) ->setDiff($object); return $adapter; } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { $xactions = array(); return $xactions; } private function updateDiffFromDict(DifferentialDiff $diff, $dict) { $diff ->setSourcePath(idx($dict, 'sourcePath')) ->setSourceMachine(idx($dict, 'sourceMachine')) ->setBranch(idx($dict, 'branch')) ->setCreationMethod(idx($dict, 'creationMethod')) ->setAuthorPHID(idx($dict, 'authorPHID', $this->getActor())) ->setBookmark(idx($dict, 'bookmark')) ->setRepositoryPHID(idx($dict, 'repositoryPHID')) ->setRepositoryUUID(idx($dict, 'repositoryUUID')) ->setSourceControlSystem(idx($dict, 'sourceControlSystem')) ->setSourceControlPath(idx($dict, 'sourceControlPath')) ->setSourceControlBaseRevision(idx($dict, 'sourceControlBaseRevision')) ->setLintStatus(idx($dict, 'lintStatus')) ->setUnitStatus(idx($dict, 'unitStatus')) ->setArcanistProjectPHID(idx($dict, 'arcanistProjectPHID')); return $diff; } } diff --git a/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php b/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php index dc4acb383b..111d6ba91f 100644 --- a/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php +++ b/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php @@ -1,289 +1,291 @@ setOldOffset($old_offset) ->setOldLen($old_len) ->setNewOffset($new_offset) ->setNewLen($new_len) ->setChanges($changes); return $hunk; } // Returns a change that consists of a single hunk, starting at line 1. private function createSingleChange($old_lines, $new_lines, $changes) { return array( 0 => $this->createHunk(1, $old_lines, 1, $new_lines, $changes), ); } private function createHunksFromFile($name) { $data = Filesystem::readFile(dirname(__FILE__).'/data/'.$name); $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($data); if (count($changes) !== 1) { throw new Exception("Expected 1 changeset for '{$name}'!"); } - $diff = DifferentialDiff::newFromRawChanges($changes); + $diff = DifferentialDiff::newFromRawChanges( + PhabricatorUser::getOmnipotentUser(), + $changes); return head($diff->getChangesets())->getHunks(); } public function testOneLineOldComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(1, 0, '-a'); $context = $parser->makeContextDiff( $hunks, 0, 1, 0, 0); $this->assertEqual("@@ -1,1 @@\n-a", $context); } public function testOneLineNewComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(0, 1, '+a'); $context = $parser->makeContextDiff( $hunks, 1, 1, 0, 0); $this->assertEqual("@@ +1,1 @@\n+a", $context); } public function testCannotFindContext() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(0, 1, '+a'); $context = $parser->makeContextDiff( $hunks, 1, 2, 0, 0); $this->assertEqual('', $context); } public function testOverlapFromStartOfHunk() { $parser = new DifferentialHunkParser(); $hunks = array( 0 => $this->createHunk(23, 2, 42, 2, " 1\n 2"), ); $context = $parser->makeContextDiff( $hunks, 1, 41, 1, 0); $this->assertEqual("@@ -23,1 +42,1 @@\n 1", $context); } public function testOverlapAfterEndOfHunk() { $parser = new DifferentialHunkParser(); $hunks = array( 0 => $this->createHunk(23, 2, 42, 2, " 1\n 2"), ); $context = $parser->makeContextDiff( $hunks, 1, 43, 1, 0); $this->assertEqual("@@ -24,1 +43,1 @@\n 2", $context); } public function testInclusionOfNewFileInOldCommentFromStart() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 3, "+n1\n". " e1/2\n". "-o2\n". "+n3\n"); $context = $parser->makeContextDiff( $hunks, 0, 1, 1, 0); $this->assertEqual( "@@ -1,2 +2,1 @@\n". " e1/2\n". "-o2", $context); } public function testInclusionOfOldFileInNewCommentFromStart() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 2, "-o1\n". " e2/1\n". "-o3\n". "+n2\n"); $context = $parser->makeContextDiff( $hunks, 1, 1, 1, 0); $this->assertEqual( "@@ -2,1 +1,2 @@\n". " e2/1\n". "+n2", $context); } public function testNoNewlineAtEndOfFile() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(0, 1, "+a\n". "\\No newline at end of file"); // Note that this only works with additional context. $context = $parser->makeContextDiff( $hunks, 1, 2, 0, 1); $this->assertEqual( "@@ +1,1 @@\n". "+a\n". "\\No newline at end of file", $context); } public function testMultiLineNewComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(7, 7, " e1\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5\n". "+n6\n". " e7\n"); $context = $parser->makeContextDiff( $hunks, 1, 2, 4, 0); $this->assertEqual( "@@ -2,5 +2,5 @@\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5\n". "+n6", $context); } public function testMultiLineOldComment() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(7, 7, " e1\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5\n". "+n6\n". " e7\n"); $context = $parser->makeContextDiff( $hunks, 0, 2, 4, 0); $this->assertEqual( "@@ -2,5 +2,4 @@\n". " e2\n". "-o3\n". "-o4\n". "+n3\n". " e5/4\n". " e6/5", $context); } public function testInclusionOfNewFileInOldCommentFromStartWithContext() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 3, "+n1\n". " e1/2\n". "-o2\n". "+n3\n"); $context = $parser->makeContextDiff( $hunks, 0, 1, 1, 1); $this->assertEqual( "@@ -1,2 +1,2 @@\n". "+n1\n". " e1/2\n". "-o2", $context); } public function testInclusionOfOldFileInNewCommentFromStartWithContext() { $parser = new DifferentialHunkParser(); $hunks = $this->createSingleChange(2, 2, "-o1\n". " e2/1\n". "-o3\n". "+n2\n"); $context = $parser->makeContextDiff( $hunks, 1, 1, 1, 1); $this->assertEqual( "@@ -1,3 +1,2 @@\n". "-o1\n". " e2/1\n". "-o3\n". "+n2", $context); } public function testMissingContext() { $tests = array( 'missing_context.diff' => array( 4 => true, ), 'missing_context_2.diff' => array( 5 => true, ), 'missing_context_3.diff' => array( 4 => true, 13 => true, ), ); foreach ($tests as $name => $expect) { $hunks = $this->createHunksFromFile($name); $parser = new DifferentialHunkParser(); $actual = $parser->getHunkStartLines($hunks); $this->assertEqual($expect, $actual, $name); } } } diff --git a/src/applications/differential/query/DifferentialDiffQuery.php b/src/applications/differential/query/DifferentialDiffQuery.php index f9666400f1..4fc3ffcdfe 100644 --- a/src/applications/differential/query/DifferentialDiffQuery.php +++ b/src/applications/differential/query/DifferentialDiffQuery.php @@ -1,157 +1,156 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withRevisionIDs(array $revision_ids) { $this->revisionIDs = $revision_ids; return $this; } public function needChangesets($bool) { $this->needChangesets = $bool; return $this; } public function needArcanistProjects($bool) { $this->needArcanistProjects = $bool; return $this; } public function loadPage() { $table = new DifferentialDiff(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } public function willFilterPage(array $diffs) { $revision_ids = array_filter(mpull($diffs, 'getRevisionID')); $revisions = array(); if ($revision_ids) { $revisions = id(new DifferentialRevisionQuery()) ->setViewer($this->getViewer()) ->withIDs($revision_ids) ->execute(); } foreach ($diffs as $key => $diff) { if (!$diff->getRevisionID()) { - $diff->attachRevision(null); continue; } $revision = idx($revisions, $diff->getRevisionID()); if ($revision) { $diff->attachRevision($revision); continue; } unset($diffs[$key]); } if ($diffs && $this->needChangesets) { $diffs = $this->loadChangesets($diffs); } if ($diffs && $this->needArcanistProjects) { $diffs = $this->loadArcanistProjects($diffs); } return $diffs; } private function loadChangesets(array $diffs) { id(new DifferentialChangesetQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withDiffs($diffs) ->needAttachToDiffs(true) ->needHunks(true) ->execute(); return $diffs; } private function loadArcanistProjects(array $diffs) { $phids = array_filter(mpull($diffs, 'getArcanistProjectPHID')); $projects = array(); $project_map = array(); if ($phids) { $projects = id(new PhabricatorRepositoryArcanistProject()) ->loadAllWhere( 'phid IN (%Ls)', $phids); $project_map = mpull($projects, null, 'getPHID'); } foreach ($diffs as $diff) { $project = null; if ($diff->getArcanistProjectPHID()) { $project = idx($project_map, $diff->getArcanistProjectPHID()); } $diff->attachArcanistProject($project); } return $diffs; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->revisionIDs) { $where[] = qsprintf( $conn_r, 'revisionID IN (%Ld)', $this->revisionIDs); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorDifferentialApplication'; } } diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index d52df5c2ec..c16a9f85fd 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -1,446 +1,469 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'revisionID' => 'id?', 'authorPHID' => 'phid?', 'repositoryPHID' => 'phid?', 'sourceMachine' => 'text255?', 'sourcePath' => 'text255?', 'sourceControlSystem' => 'text64?', 'sourceControlBaseRevision' => 'text255?', 'sourceControlPath' => 'text255?', 'lintStatus' => 'uint32', 'unitStatus' => 'uint32', 'lineCount' => 'uint32', 'branch' => 'text255?', 'bookmark' => 'text255?', 'arcanistProjectPHID' => 'phid?', 'repositoryUUID' => 'text64?', // T6203/NULLABILITY // These should be non-null; all diffs should have a creation method // and the description should just be empty. 'creationMethod' => 'text255?', 'description' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'revisionID' => array( 'columns' => array('revisionID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialDiffPHIDType::TYPECONST); } public function addUnsavedChangeset(DifferentialChangeset $changeset) { if ($this->changesets === null) { $this->changesets = array(); } $this->unsavedChangesets[] = $changeset; $this->changesets[] = $changeset; return $this; } public function attachChangesets(array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $this->changesets = $changesets; return $this; } public function getChangesets() { return $this->assertAttached($this->changesets); } public function loadChangesets() { if (!$this->getID()) { return array(); } return id(new DifferentialChangeset())->loadAllWhere( 'diffID = %d', $this->getID()); } public function attachArcanistProject( PhabricatorRepositoryArcanistProject $project = null) { $this->arcanistProject = $project; return $this; } public function getArcanistProject() { return $this->assertAttached($this->arcanistProject); } public function getArcanistProjectName() { $name = ''; if ($this->arcanistProject) { $project = $this->getArcanistProject(); $name = $project->getName(); } return $name; } public function save() { $this->openTransaction(); $ret = parent::save(); foreach ($this->unsavedChangesets as $changeset) { $changeset->setDiffID($this->getID()); $changeset->save(); } $this->saveTransaction(); return $ret; } - public static function newFromRawChanges(array $changes) { + public static function initializeNewDiff(PhabricatorUser $actor) { + $app = id(new PhabricatorApplicationQuery()) + ->setViewer($actor) + ->withClasses(array('PhabricatorDifferentialApplication')) + ->executeOne(); + $view_policy = $app->getPolicy( + DifferentialDefaultViewCapability::CAPABILITY); + + $diff = id(new DifferentialDiff()) + ->setViewPolicy($view_policy); + + return $diff; + } + + public static function newFromRawChanges( + PhabricatorUser $actor, + array $changes) { + assert_instances_of($changes, 'ArcanistDiffChange'); - $diff = new DifferentialDiff(); + $diff = self::initializeNewDiff($actor); // There may not be any changes; initialize the changesets list so that // we don't throw later when accessing it. $diff->attachChangesets(array()); $lines = 0; foreach ($changes as $change) { if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) { // If a user pastes a diff into Differential which includes a commit // message (e.g., they ran `git show` to generate it), discard that // change when constructing a DifferentialDiff. continue; } $changeset = new DifferentialChangeset(); $add_lines = 0; $del_lines = 0; $first_line = PHP_INT_MAX; $hunks = $change->getHunks(); if ($hunks) { foreach ($hunks as $hunk) { $dhunk = new DifferentialHunkModern(); $dhunk->setOldOffset($hunk->getOldOffset()); $dhunk->setOldLen($hunk->getOldLength()); $dhunk->setNewOffset($hunk->getNewOffset()); $dhunk->setNewLen($hunk->getNewLength()); $dhunk->setChanges($hunk->getCorpus()); $changeset->addUnsavedHunk($dhunk); $add_lines += $hunk->getAddLines(); $del_lines += $hunk->getDelLines(); $added_lines = $hunk->getChangedLines('new'); if ($added_lines) { $first_line = min($first_line, head_key($added_lines)); } } $lines += $add_lines + $del_lines; } else { // This happens when you add empty files. $changeset->attachHunks(array()); } $metadata = $change->getAllMetadata(); if ($first_line != PHP_INT_MAX) { $metadata['line:first'] = $first_line; } $changeset->setOldFile($change->getOldPath()); $changeset->setFilename($change->getCurrentPath()); $changeset->setChangeType($change->getType()); $changeset->setFileType($change->getFileType()); $changeset->setMetadata($metadata); $changeset->setOldProperties($change->getOldProperties()); $changeset->setNewProperties($change->getNewProperties()); $changeset->setAwayPaths($change->getAwayPaths()); $changeset->setAddLines($add_lines); $changeset->setDelLines($del_lines); $diff->addUnsavedChangeset($changeset); } $diff->setLineCount($lines); $parser = new DifferentialChangesetParser(); $changesets = $parser->detectCopiedCode( $diff->getChangesets(), $min_width = 30, $min_lines = 3); $diff->attachChangesets($changesets); return $diff; } public function getDiffDict() { $dict = array( 'id' => $this->getID(), 'revisionID' => $this->getRevisionID(), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), 'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(), 'sourceControlPath' => $this->getSourceControlPath(), 'sourceControlSystem' => $this->getSourceControlSystem(), 'branch' => $this->getBranch(), 'bookmark' => $this->getBookmark(), 'creationMethod' => $this->getCreationMethod(), 'description' => $this->getDescription(), 'unitStatus' => $this->getUnitStatus(), 'lintStatus' => $this->getLintStatus(), 'changes' => array(), 'properties' => array(), 'projectName' => $this->getArcanistProjectName(), ); $dict['changes'] = $this->buildChangesList(); $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($properties as $property) { $dict['properties'][$property->getName()] = $property->getData(); if ($property->getName() == 'local:commits') { foreach ($property->getData() as $commit) { $dict['authorName'] = $commit['author']; $dict['authorEmail'] = idx($commit, 'authorEmail'); break; } } } return $dict; } public function buildChangesList() { $changes = array(); foreach ($this->getChangesets() as $changeset) { $hunks = array(); foreach ($changeset->getHunks() as $hunk) { $hunks[] = array( 'oldOffset' => $hunk->getOldOffset(), 'newOffset' => $hunk->getNewOffset(), 'oldLength' => $hunk->getOldLen(), 'newLength' => $hunk->getNewLen(), 'addLines' => null, 'delLines' => null, 'isMissingOldNewline' => null, 'isMissingNewNewline' => null, 'corpus' => $hunk->getChanges(), ); } $change = array( 'id' => $changeset->getID(), 'metadata' => $changeset->getMetadata(), 'oldPath' => $changeset->getOldFile(), 'currentPath' => $changeset->getFilename(), 'awayPaths' => $changeset->getAwayPaths(), 'oldProperties' => $changeset->getOldProperties(), 'newProperties' => $changeset->getNewProperties(), 'type' => $changeset->getChangeType(), 'fileType' => $changeset->getFileType(), 'commitHash' => null, 'addLines' => $changeset->getAddLines(), 'delLines' => $changeset->getDelLines(), 'hunks' => $hunks, ); $changes[] = $change; } return $changes; } + public function hasRevision() { + return $this->revision !== self::ATTACHABLE; + } + public function getRevision() { return $this->assertAttached($this->revision); } public function attachRevision(DifferentialRevision $revision = null) { $this->revision = $revision; return $this; } public function attachProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key) { return $this->assertAttachedKey($this->properties, $key); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { - if ($this->getRevision()) { + if ($this->hasRevision()) { return $this->getRevision()->getPolicy($capability); } - return PhabricatorPolicies::POLICY_USER; + return $this->viewPolicy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - if ($this->getRevision()) { + if ($this->hasRevision()) { return $this->getRevision()->hasAutomaticCapability($capability, $viewer); } - return false; + return ($this->getAuthorPHID() == $viewer->getPhid()); } public function describeAutomaticCapability($capability) { - if ($this->getRevision()) { + if ($this->hasRevision()) { return pht( 'This diff is attached to a revision, and inherits its policies.'); } - return null; + return pht('The author of a diff can see it.'); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildablePHID() { return $this->getPHID(); } public function getHarbormasterContainerPHID() { if ($this->getRevisionID()) { $revision = id(new DifferentialRevision())->load($this->getRevisionID()); if ($revision) { return $revision->getPHID(); } } return null; } public function getBuildVariables() { $results = array(); $results['buildable.diff'] = $this->getID(); $revision = $this->getRevision(); $results['buildable.revision'] = $revision->getID(); $repo = $revision->getRepository(); if ($repo) { $results['repository.callsign'] = $repo->getCallsign(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); } return $results; } public function getAvailableBuildVariables() { return array( 'buildable.diff' => pht('The differential diff ID, if applicable.'), 'buildable.revision' => pht('The differential revision ID, if applicable.'), 'repository.callsign' => pht('The callsign 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.'), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { if (!$this->getRevisionID()) { return null; } return $this->getRevision()->getApplicationTransactionEditor(); } public function getApplicationTransactionObject() { if (!$this->getRevisionID()) { return null; } return $this->getRevision(); } public function getApplicationTransactionTemplate() { if (!$this->getRevisionID()) { return null; } return $this->getRevision()->getApplicationTransactionTemplate(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); foreach ($this->loadChangesets() as $changeset) { $changeset->delete(); } $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($properties as $prop) { $prop->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/differential/storage/__tests__/DifferentialDiffTestCase.php b/src/applications/differential/storage/__tests__/DifferentialDiffTestCase.php index e9e21784e7..3f851d767c 100644 --- a/src/applications/differential/storage/__tests__/DifferentialDiffTestCase.php +++ b/src/applications/differential/storage/__tests__/DifferentialDiffTestCase.php @@ -1,55 +1,58 @@ parseDiff(Filesystem::readFile($root.'lint_engine.diff'))); $copies = idx(head($diff->getChangesets())->getMetadata(), 'copy:lines'); $this->assertEqual( array_combine(range(237, 252), range(167, 182)), ipull($copies, 1)); } public function testDetectSlowCopiedCode() { // This tests that the detector has a reasonable runtime when a diff // contains a very large number of identical lines. See T5041. $parser = new ArcanistDiffParser(); $line = str_repeat('x', 60); $oline = '-'.$line."\n"; $nline = '+'.$line."\n"; $n = 1000; $oblock = str_repeat($oline, $n); $nblock = str_repeat($nline, $n); $raw_diff = <<parseDiff($raw_diff)); + $diff = DifferentialDiff::newFromRawChanges( + PhabricatorUser::getOmnipotentUser(), + $parser->parseDiff($raw_diff)); $this->assertTrue(true); } } diff --git a/src/applications/diffusion/controller/DiffusionChangeController.php b/src/applications/diffusion/controller/DiffusionChangeController.php index 37e1dfb02d..c510b403cd 100644 --- a/src/applications/diffusion/controller/DiffusionChangeController.php +++ b/src/applications/diffusion/controller/DiffusionChangeController.php @@ -1,163 +1,165 @@ diffusionRequest; $viewer = $this->getRequest()->getUser(); $content = array(); $data = $this->callConduitWithDiffusionRequest( 'diffusion.diffquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), )); $drequest->updateSymbolicCommit($data['effectiveCommit']); $raw_changes = ArcanistDiffChange::newFromConduit($data['changes']); - $diff = DifferentialDiff::newFromRawChanges($raw_changes); + $diff = DifferentialDiff::newFromRawChanges( + $viewer, + $raw_changes); $changesets = $diff->getChangesets(); $changeset = reset($changesets); if (!$changeset) { // TODO: Refine this. return new Aphront404Response(); } $repository = $drequest->getRepository(); $callsign = $repository->getCallsign(); $changesets = array( 0 => $changeset, ); $changeset_view = new DifferentialChangesetListView(); $changeset_view->setTitle(pht('Change')); $changeset_view->setChangesets($changesets); $changeset_view->setVisibleChangesets($changesets); $changeset_view->setRenderingReferences( array( 0 => $drequest->generateURI(array('action' => 'rendering-ref')), )); $raw_params = array( 'action' => 'browse', 'params' => array( 'view' => 'raw', ), ); $right_uri = $drequest->generateURI($raw_params); $raw_params['params']['before'] = $drequest->getStableCommit(); $left_uri = $drequest->generateURI($raw_params); $changeset_view->setRawFileURIs($left_uri, $right_uri); $changeset_view->setRenderURI('/diffusion/'.$callsign.'/diff/'); $changeset_view->setWhitespace( DifferentialChangesetParser::WHITESPACE_SHOW_ALL); $changeset_view->setUser($this->getRequest()->getUser()); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); $content[] = $changeset_view->render(); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'change', )); $links = $this->renderPathLinks($drequest, $mode = 'browse'); $header = id(new PHUIHeaderView()) ->setHeader($links) ->setUser($viewer) ->setPolicyObject($drequest->getRepository()); $actions = $this->buildActionView($drequest); $properties = $this->buildPropertyView($drequest, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $content, ), array( 'title' => pht('Change'), 'device' => false, )); } private function buildActionView(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); $view = id(new PhabricatorActionListView()) ->setUser($viewer); $history_uri = $drequest->generateURI( array( 'action' => 'history', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($history_uri) ->setIcon('fa-clock-o')); $browse_uri = $drequest->generateURI( array( 'action' => 'browse', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Browse Content')) ->setHref($browse_uri) ->setIcon('fa-files-o')); return $view; } protected function buildPropertyView( DiffusionRequest $drequest, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setActionList($actions); $stable_commit = $drequest->getStableCommit(); $callsign = $drequest->getRepository()->getCallsign(); $view->addProperty( pht('Commit'), phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $stable_commit, )), ), $drequest->getRepository()->formatCommitName($stable_commit))); return $view; } } diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index 3e7308a56a..f4006f82c7 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -1,1108 +1,1109 @@ getRequest()->getUser(); $drequest = DiffusionRequest::newFromDictionary($data); $this->diffusionRequest = $drequest; } public function processRequest() { $drequest = $this->getDiffusionRequest(); $request = $this->getRequest(); $user = $request->getUser(); if ($request->getStr('diff')) { return $this->buildRawDiffResponse($drequest); } $repository = $drequest->getRepository(); $callsign = $repository->getCallsign(); $content = array(); $commit = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->withRepository($repository) ->withIdentifiers(array($drequest->getCommit())) ->needCommitData(true) ->needAuditRequests(true) ->executeOne(); $crumbs = $this->buildCrumbs(array( 'commit' => true, )); if (!$commit) { $exists = $this->callConduitWithDiffusionRequest( 'diffusion.existsquery', array('commit' => $drequest->getCommit())); if (!$exists) { return new Aphront404Response(); } $error = id(new AphrontErrorView()) ->setTitle(pht('Commit Still Parsing')) ->appendChild( pht( 'Failed to load the commit because the commit has not been '. 'parsed yet.')); return $this->buildApplicationPage( array( $crumbs, $error, ), array( 'title' => pht('Commit Still Parsing'), 'device' => false, )); } $top_anchor = id(new PhabricatorAnchorView()) ->setAnchorName('top') ->setNavigationMarker(true); $audit_requests = $commit->getAudits(); $this->auditAuthorityPHIDs = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user); $commit_data = $commit->getCommitData(); $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub'); $changesets = null; if ($is_foreign) { $subpath = $commit_data->getCommitDetail('svn-subpath'); $error_panel = new AphrontErrorView(); $error_panel->setTitle(pht('Commit Not Tracked')); $error_panel->setSeverity(AphrontErrorView::SEVERITY_WARNING); $error_panel->appendChild( pht("This Diffusion repository is configured to track only one ". "subdirectory of the entire Subversion repository, and this commit ". "didn't affect the tracked subdirectory ('%s'), so no ". "information is available.", $subpath)); $content[] = $error_panel; $content[] = $top_anchor; } else { $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $user); require_celerity_resource('phabricator-remarkup-css'); $parents = $this->callConduitWithDiffusionRequest( 'diffusion.commitparentsquery', array('commit' => $drequest->getCommit())); if ($parents) { $parents = id(new DiffusionCommitQuery()) ->setViewer($user) ->withRepository($repository) ->withIdentifiers($parents) ->execute(); } $headsup_view = id(new PHUIHeaderView()) ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))); $headsup_actions = $this->renderHeadsupActionList($commit, $repository); $commit_properties = $this->loadCommitProperties( $commit, $commit_data, $parents, $audit_requests); $property_list = id(new PHUIPropertyListView()) ->setHasKeyboardShortcuts(true) ->setUser($user) ->setObject($commit); foreach ($commit_properties as $key => $value) { $property_list->addProperty($key, $value); } $message = $commit_data->getCommitMessage(); $revision = $commit->getCommitIdentifier(); $message = $this->linkBugtraq($message); $message = $engine->markupText($message); $property_list->invokeWillRenderEvent(); $property_list->setActionList($headsup_actions); $detail_list = new PHUIPropertyListView(); $detail_list->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $detail_list->addTextContent( phutil_tag( 'div', array( 'class' => 'diffusion-commit-message phabricator-remarkup', ), $message)); $content[] = $top_anchor; $object_box = id(new PHUIObjectBoxView()) ->setHeader($headsup_view) ->addPropertyList($property_list) ->addPropertyList($detail_list); $content[] = $object_box; } $content[] = $this->buildComments($commit); $hard_limit = 1000; if ($commit->isImported()) { $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $change_query->setLimit($hard_limit + 1); $changes = $change_query->loadChanges(); } else { $changes = array(); } $was_limited = (count($changes) > $hard_limit); if ($was_limited) { $changes = array_slice($changes, 0, $hard_limit); } $content[] = $this->buildMergesTable($commit); $highlighted_audits = $commit->getAuthorityAudits( $user, $this->auditAuthorityPHIDs); $owners_paths = array(); if ($highlighted_audits) { $packages = id(new PhabricatorOwnersPackage())->loadAllWhere( 'phid IN (%Ls)', mpull($highlighted_audits, 'getAuditorPHID')); if ($packages) { $owners_paths = id(new PhabricatorOwnersPath())->loadAllWhere( 'repositoryPHID = %s AND packageID IN (%Ld)', $repository->getPHID(), mpull($packages, 'getID')); } } $change_table = new DiffusionCommitChangeTableView(); $change_table->setDiffusionRequest($drequest); $change_table->setPathChanges($changes); $change_table->setOwnersPaths($owners_paths); $count = count($changes); $bad_commit = null; if ($count == 0) { $bad_commit = queryfx_one( id(new PhabricatorRepository())->establishConnection('r'), 'SELECT * FROM %T WHERE fullCommitName = %s', PhabricatorRepository::TABLE_BADCOMMIT, 'r'.$callsign.$commit->getCommitIdentifier()); } if ($bad_commit) { $content[] = $this->renderStatusMessage( pht('Bad Commit'), $bad_commit['description']); } else if ($is_foreign) { // Don't render anything else. } else if (!$commit->isImported()) { $content[] = $this->renderStatusMessage( pht('Still Importing...'), pht( 'This commit is still importing. Changes will be visible once '. 'the import finishes.')); } else if (!count($changes)) { $content[] = $this->renderStatusMessage( pht('Empty Commit'), pht( 'This commit is empty and does not affect any paths.')); } else if ($was_limited) { $content[] = $this->renderStatusMessage( pht('Enormous Commit'), pht( 'This commit is enormous, and affects more than %d files. '. 'Changes are not shown.', $hard_limit)); } else { // The user has clicked "Show All Changes", and we should show all the // changes inline even if there are more than the soft limit. $show_all_details = $request->getBool('show_all'); $change_panel = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $header->setHeader('Changes ('.number_format($count).')'); $change_panel->setID('toc'); if ($count > self::CHANGES_LIMIT && !$show_all_details) { $icon = id(new PHUIIconView()) ->setIconFont('fa-files-o'); $button = id(new PHUIButtonView()) ->setText(pht('Show All Changes')) ->setHref('?show_all=true') ->setTag('a') ->setIcon($icon); $warning_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->setTitle('Very Large Commit') ->appendChild( pht('This commit is very large. Load each file individually.')); $change_panel->setErrorView($warning_view); $header->addActionLink($button); } $change_panel->appendChild($change_table); $change_panel->setHeader($header); $content[] = $change_panel; $changesets = DiffusionPathChange::convertToDifferentialChangesets( + $user, $changes); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $vcs_supports_directory_changes = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $vcs_supports_directory_changes = false; break; default: throw new Exception('Unknown VCS.'); } $references = array(); foreach ($changesets as $key => $changeset) { $file_type = $changeset->getFileType(); if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { if (!$vcs_supports_directory_changes) { unset($changesets[$key]); continue; } } $references[$key] = $drequest->generateURI( array( 'action' => 'rendering-ref', 'path' => $changeset->getFilename(), )); } // TODO: Some parts of the views still rely on properties of the // DifferentialChangeset. Make the objects ephemeral to make sure we don't // accidentally save them, and then set their ID to the appropriate ID for // this application (the path IDs). $path_ids = array_flip(mpull($changes, 'getPath')); foreach ($changesets as $changeset) { $changeset->makeEphemeral(); $changeset->setID($path_ids[$changeset->getFilename()]); } if ($count <= self::CHANGES_LIMIT || $show_all_details) { $visible_changesets = $changesets; } else { $visible_changesets = array(); $inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments( $user, $commit->getPHID()); $path_ids = mpull($inlines, null, 'getPathID'); foreach ($changesets as $key => $changeset) { if (array_key_exists($changeset->getID(), $path_ids)) { $visible_changesets[$key] = $changeset; } } } $change_list_title = DiffusionView::nameCommit( $repository, $commit->getCommitIdentifier()); $change_list = new DifferentialChangesetListView(); $change_list->setTitle($change_list_title); $change_list->setChangesets($changesets); $change_list->setVisibleChangesets($visible_changesets); $change_list->setRenderingReferences($references); $change_list->setRenderURI('/diffusion/'.$callsign.'/diff/'); $change_list->setRepository($repository); $change_list->setUser($user); // TODO: Try to setBranch() to something reasonable here? $change_list->setStandaloneURI( '/diffusion/'.$callsign.'/diff/'); $change_list->setRawFileURIs( // TODO: Implement this, somewhat tricky if there's an octopus merge // or whatever? null, '/diffusion/'.$callsign.'/diff/?view=r'); $change_list->setInlineCommentControllerURI( '/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/'); $change_references = array(); foreach ($changesets as $key => $changeset) { $change_references[$changeset->getID()] = $references[$key]; } $change_table->setRenderingReferences($change_references); $content[] = $change_list->render(); } $content[] = $this->renderAddCommentPanel($commit, $audit_requests); $commit_id = 'r'.$callsign.$commit->getCommitIdentifier(); $short_name = DiffusionView::nameCommit( $repository, $commit->getCommitIdentifier()); $prefs = $user->loadPreferences(); $pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE; $pref_collapse = PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED; $show_filetree = $prefs->getPreference($pref_filetree); $collapsed = $prefs->getPreference($pref_collapse); if ($changesets && $show_filetree) { $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setAnchorName('top') ->setTitle($short_name) ->setBaseURI(new PhutilURI('/'.$commit_id)) ->build($changesets) ->setCrumbs($crumbs) ->setCollapsed((bool)$collapsed) ->appendChild($content); $content = $nav; } else { $content = array($crumbs, $content); } return $this->buildApplicationPage( $content, array( 'title' => $commit_id, 'pageObjects' => array($commit->getPHID()), 'device' => false, )); } private function loadCommitProperties( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data, array $parents, array $audit_requests) { assert_instances_of($parents, 'PhabricatorRepositoryCommit'); $viewer = $this->getRequest()->getUser(); $commit_phid = $commit->getPHID(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($commit_phid)) ->withEdgeTypes(array( DiffusionCommitHasTaskEdgeType::EDGECONST, PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT, PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV, )); $edges = $edge_query->execute(); $task_phids = array_keys( $edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]); $proj_phids = array_keys( $edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT]); $revision_phid = key( $edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV]); $phids = $edge_query->getDestinationPHIDs(array($commit_phid)); if ($data->getCommitDetail('authorPHID')) { $phids[] = $data->getCommitDetail('authorPHID'); } if ($data->getCommitDetail('reviewerPHID')) { $phids[] = $data->getCommitDetail('reviewerPHID'); } if ($data->getCommitDetail('committerPHID')) { $phids[] = $data->getCommitDetail('committerPHID'); } if ($parents) { foreach ($parents as $parent) { $phids[] = $parent->getPHID(); } } // NOTE: We should never normally have more than a single push log, but // it can occur naturally if a commit is pushed, then the branch it was // on is deleted, then the commit is pushed again (or through other similar // chains of events). This should be rare, but does not indicate a bug // or data issue. // NOTE: We never query push logs in SVN because the commiter is always // the pusher and the commit time is always the push time; the push log // is redundant and we save a query by skipping it. $push_logs = array(); if ($repository->isHosted() && !$repository->isSVN()) { $push_logs = id(new PhabricatorRepositoryPushLogQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withNewRefs(array($commit->getCommitIdentifier())) ->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)) ->execute(); foreach ($push_logs as $log) { $phids[] = $log->getPusherPHID(); } } $handles = array(); if ($phids) { $handles = $this->loadViewerHandles($phids); } $props = array(); if ($commit->getAuditStatus()) { $status = PhabricatorAuditCommitStatusConstants::getStatusName( $commit->getAuditStatus()); $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE) ->setName($status); switch ($commit->getAuditStatus()) { case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT: $tag->setBackgroundColor(PHUITagView::COLOR_ORANGE); break; case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED: $tag->setBackgroundColor(PHUITagView::COLOR_RED); break; case PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED: $tag->setBackgroundColor(PHUITagView::COLOR_BLUE); break; case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED: $tag->setBackgroundColor(PHUITagView::COLOR_GREEN); break; } $props['Status'] = $tag; } if ($audit_requests) { $user_requests = array(); $other_requests = array(); foreach ($audit_requests as $audit_request) { if ($audit_request->isUser()) { $user_requests[] = $audit_request; } else { $other_requests[] = $audit_request; } } if ($user_requests) { $props['Auditors'] = $this->renderAuditStatusView( $user_requests); } if ($other_requests) { $props['Project/Package Auditors'] = $this->renderAuditStatusView( $other_requests); } } $author_phid = $data->getCommitDetail('authorPHID'); $author_name = $data->getAuthorName(); if (!$repository->isSVN()) { $authored_info = id(new PHUIStatusItemView()); // TODO: In Git, a distinct authorship date is available. When present, // we should show it here. if ($author_phid) { $authored_info->setTarget($handles[$author_phid]->renderLink()); } else if (strlen($author_name)) { $authored_info->setTarget($author_name); } $props['Authored'] = id(new PHUIStatusListView()) ->addItem($authored_info); } $committed_info = id(new PHUIStatusItemView()) ->setNote(phabricator_datetime($commit->getEpoch(), $viewer)); $committer_phid = $data->getCommitDetail('committerPHID'); $committer_name = $data->getCommitDetail('committer'); if ($committer_phid) { $committed_info->setTarget($handles[$committer_phid]->renderLink()); } else if (strlen($committer_name)) { $committed_info->setTarget($committer_name); } else if ($author_phid) { $committed_info->setTarget($handles[$author_phid]->renderLink()); } else if (strlen($author_name)) { $committed_info->setTarget($author_name); } $props['Committed'] = id(new PHUIStatusListView()) ->addItem($committed_info); if ($push_logs) { $pushed_list = new PHUIStatusListView(); foreach ($push_logs as $push_log) { $pushed_item = id(new PHUIStatusItemView()) ->setTarget($handles[$push_log->getPusherPHID()]->renderLink()) ->setNote(phabricator_datetime($push_log->getEpoch(), $viewer)); $pushed_list->addItem($pushed_item); } $props['Pushed'] = $pushed_list; } $reviewer_phid = $data->getCommitDetail('reviewerPHID'); if ($reviewer_phid) { $props['Reviewer'] = $handles[$reviewer_phid]->renderLink(); } if ($revision_phid) { $props['Differential Revision'] = $handles[$revision_phid]->renderLink(); } if ($parents) { $parent_links = array(); foreach ($parents as $parent) { $parent_links[] = $handles[$parent->getPHID()]->renderLink(); } $props['Parents'] = phutil_implode_html(" \xC2\xB7 ", $parent_links); } $props['Branches'] = phutil_tag( 'span', array( 'id' => 'commit-branches', ), pht('Unknown')); $props['Tags'] = phutil_tag( 'span', array( 'id' => 'commit-tags', ), pht('Unknown')); $callsign = $repository->getCallsign(); $root = '/diffusion/'.$callsign.'/commit/'.$commit->getCommitIdentifier(); Javelin::initBehavior( 'diffusion-commit-branches', array( $root.'/branches/' => 'commit-branches', $root.'/tags/' => 'commit-tags', )); $refs = $this->buildRefs($drequest); if ($refs) { $props['References'] = $refs; } if ($task_phids) { $task_list = array(); foreach ($task_phids as $phid) { $task_list[] = $handles[$phid]->renderLink(); } $task_list = phutil_implode_html(phutil_tag('br'), $task_list); $props['Tasks'] = $task_list; } if ($proj_phids) { $proj_list = array(); foreach ($proj_phids as $phid) { $proj_list[] = $handles[$phid]->renderLink(); } $proj_list = phutil_implode_html(phutil_tag('br'), $proj_list); $props['Projects'] = $proj_list; } return $props; } private function buildComments(PhabricatorRepositoryCommit $commit) { $viewer = $this->getRequest()->getUser(); $xactions = id(new PhabricatorAuditTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($commit->getPHID())) ->needComments(true) ->execute(); $path_ids = array(); foreach ($xactions as $xaction) { if ($xaction->hasComment()) { $path_id = $xaction->getComment()->getPathID(); if ($path_id) { $path_ids[] = $path_id; } } } $path_map = array(); if ($path_ids) { $path_map = id(new DiffusionPathQuery()) ->withPathIDs($path_ids) ->execute(); $path_map = ipull($path_map, 'path', 'id'); } return id(new PhabricatorAuditTransactionView()) ->setUser($viewer) ->setObjectPHID($commit->getPHID()) ->setPathMap($path_map) ->setTransactions($xactions); } private function renderAddCommentPanel( PhabricatorRepositoryCommit $commit, array $audit_requests) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $request = $this->getRequest(); $user = $request->getUser(); if (!$user->isLoggedIn()) { return id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setRequestURI($request->getRequestURI()); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $pane_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'differential-keyboard-navigation', array( 'haunt' => $pane_id, )); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), 'diffusion-audit-'.$commit->getID()); if ($draft) { $draft = $draft->getDraft(); } else { $draft = null; } $actions = $this->getAuditActions($commit, $audit_requests); $form = id(new AphrontFormView()) ->setUser($user) ->setAction('/audit/addcomment/') ->addHiddenInput('commit', $commit->getPHID()) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Action')) ->setName('action') ->setID('audit-action') ->setOptions($actions)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Add Auditors')) ->setName('auditors') ->setControlID('add-auditors') ->setControlStyle('display: none') ->setID('add-auditors-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Add CCs')) ->setName('ccs') ->setControlID('add-ccs') ->setControlStyle('display: none') ->setID('add-ccs-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Comments')) ->setName('content') ->setValue($draft) ->setID('audit-content') ->setUser($user)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Submit'))); $header = new PHUIHeaderView(); $header->setHeader( $is_serious ? pht('Audit Commit') : pht('Creative Accounting')); require_celerity_resource('phabricator-transaction-view-css'); $mailable_source = new PhabricatorMetaMTAMailableDatasource(); $auditor_source = new DiffusionAuditorDatasource(); Javelin::initBehavior( 'differential-add-reviewers-and-ccs', array( 'dynamic' => array( 'add-auditors-tokenizer' => array( 'actions' => array('add_auditors' => 1), 'src' => $auditor_source->getDatasourceURI(), 'row' => 'add-auditors', 'placeholder' => $auditor_source->getPlaceholderText(), ), 'add-ccs-tokenizer' => array( 'actions' => array('add_ccs' => 1), 'src' => $mailable_source->getDatasourceURI(), 'row' => 'add-ccs', 'placeholder' => $mailable_source->getPlaceholderText(), ), ), 'select' => 'audit-action', )); Javelin::initBehavior('differential-feedback-preview', array( 'uri' => '/audit/preview/'.$commit->getID().'/', 'preview' => 'audit-preview', 'content' => 'audit-content', 'action' => 'audit-action', 'previewTokenizers' => array( 'auditors' => 'add-auditors-tokenizer', 'ccs' => 'add-ccs-tokenizer', ), 'inline' => 'inline-comment-preview', 'inlineuri' => '/diffusion/inline/preview/'.$commit->getPHID().'/', )); $loading = phutil_tag_div( 'aphront-panel-preview-loading-text', pht('Loading preview...')); $preview_panel = phutil_tag_div( 'aphront-panel-preview aphront-panel-flush', array( phutil_tag('div', array('id' => 'audit-preview'), $loading), phutil_tag('div', array('id' => 'inline-comment-preview')), )); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); $anchor = id(new PhabricatorAnchorView()) ->setAnchorName('comment') ->setNavigationMarker(true) ->render(); $comment_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($form); return phutil_tag( 'div', array( 'id' => $pane_id, ), phutil_tag_div( 'differential-add-comment-panel', array($anchor, $comment_box, $preview_panel))); } /** * Return a map of available audit actions for rendering into a