diff --git a/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php
index 219f5e6dd2..68531ea6bc 100644
--- a/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php
@@ -1,69 +1,70 @@
'required string',
'repositoryPHID' => '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_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),);
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 d2493b9f60..c092597fbb 100644
--- a/src/applications/differential/controller/DifferentialDiffCreateController.php
+++ b/src/applications/differential/controller/DifferentialDiffCreateController.php
@@ -1,108 +1,127 @@
getRequest();
$diff = null;
+ $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());
$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));
+ }
+
$form
->setAction('/differential/diff/create/')
->setEncType('multipart/form-data')
->setUser($request->getUser())
->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 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/DifferentialDiffViewController.php b/src/applications/differential/controller/DifferentialDiffViewController.php
index 9bb61cff3b..39f2d18b15 100644
--- a/src/applications/differential/controller/DifferentialDiffViewController.php
+++ b/src/applications/differential/controller/DifferentialDiffViewController.php
@@ -1,150 +1,153 @@
id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->executeOne();
if (!$diff) {
return new Aphront404Response();
}
$error_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
if ($diff->getRevisionID()) {
$error_view->appendChild(
pht(
'This diff belongs to revision %s.',
phutil_tag(
'a',
array(
'href' => '/D'.$diff->getRevisionID(),
),
'D'.$diff->getRevisionID())));
} else {
// TODO: implement optgroup support in AphrontFormSelectControl?
$select = array();
$select[] = hsprintf('');
$revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withAuthors(array($viewer->getPHID()))
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->execute();
if ($revisions) {
$select[] = hsprintf(
'');
}
$select = phutil_tag(
'select',
array('name' => 'revisionID'),
$select);
$form = id(new AphrontFormView())
->setUser($request->getUser())
->setAction('/differential/revision/edit/')
->addHiddenInput('diffID', $diff->getID())
->addHiddenInput('viaDiffView', 1)
+ ->addHiddenInput(
+ id(new DifferentialRepositoryField())->getFieldKey(),
+ $diff->getRepositoryPHID())
->appendRemarkupInstructions(
pht(
'Review the diff for correctness. When you are satisfied, either '.
'**create a new revision** or **update an existing revision**.'))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Attach To'))
->setValue($select))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Continue')));
$error_view->appendChild($form);
}
$props = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$diff->getID());
$props = mpull($props, 'getData', 'getName');
$property_head = id(new PHUIHeaderView())
->setHeader(pht('Properties'));
$property_view = new PHUIPropertyListView();
$changesets = $diff->loadChangesets();
$changesets = msort($changesets, 'getSortKey');
$table_of_contents = id(new DifferentialDiffTableOfContentsView())
->setChangesets($changesets)
->setVisibleChangesets($changesets)
->setUnitTestData(idx($props, 'arc:unit', array()));
$refs = array();
foreach ($changesets as $changeset) {
$refs[$changeset->getID()] = $changeset->getID();
}
$details = id(new DifferentialChangesetListView())
->setChangesets($changesets)
->setVisibleChangesets($changesets)
->setRenderingReferences($refs)
->setStandaloneURI('/differential/changeset/')
->setDiff($diff)
->setTitle(pht('Diff %d', $diff->getID()))
->setUser($request->getUser());
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Diff %d', $diff->getID()));
$prop_box = id(new PHUIObjectBoxView())
->setHeader($property_head)
->addPropertyList($property_view)
->setErrorView($error_view);
return $this->buildApplicationPage(
array(
$crumbs,
$prop_box,
$table_of_contents,
$details,
),
array(
'title' => pht('Diff View'),
));
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionEditController.php b/src/applications/differential/controller/DifferentialRevisionEditController.php
index 12dac2ca1d..14bcdf0476 100644
--- a/src/applications/differential/controller/DifferentialRevisionEditController.php
+++ b/src/applications/differential/controller/DifferentialRevisionEditController.php
@@ -1,183 +1,203 @@
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));
+ }
+ }
+
$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));
}
- $editor = id(new DifferentialTransactionEditor())
- ->setActor($viewer)
- ->setContentSourceFromRequest($request)
- ->setContinueOnNoEffect(true);
-
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 0d87df60de..c2513dab7a 100644
--- a/src/applications/differential/editor/DifferentialDiffEditor.php
+++ b/src/applications/differential/editor/DifferentialDiffEditor.php
@@ -1,236 +1,242 @@
lookupRepository = $bool;
+ return $this;
+ }
public function getEditorApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getEditorObjectsDescription() {
return pht('Differential Diffs');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$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;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialDiffTransaction::TYPE_DIFF_CREATE:
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 (!$object->getRepositoryPHID()) {
+ 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/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php
index a90ead7c0a..f2adcfc78f 100644
--- a/src/applications/differential/editor/DifferentialTransactionEditor.php
+++ b/src/applications/differential/editor/DifferentialTransactionEditor.php
@@ -1,1849 +1,1859 @@
getTransactionType() == $type_update) {
return $xaction;
}
}
return null;
}
public function setIsCloseByCommit($is_close_by_commit) {
$this->isCloseByCommit = $is_close_by_commit;
return $this;
}
public function getIsCloseByCommit() {
return $this->isCloseByCommit;
}
public function setChangedPriorToCommitURI($uri) {
$this->changedPriorToCommitURI = $uri;
return $this;
}
public function getChangedPriorToCommitURI() {
return $this->changedPriorToCommitURI;
}
+ public function setRepositoryPHIDOverride($phid_or_null) {
+ $this->repositoryPHIDOverride = $phid_or_null;
+ return $this;
+ }
+
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = DifferentialTransaction::TYPE_ACTION;
$types[] = DifferentialTransaction::TYPE_INLINE;
$types[] = DifferentialTransaction::TYPE_STATUS;
$types[] = DifferentialTransaction::TYPE_UPDATE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return $object->getViewPolicy();
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return $object->getEditPolicy();
case DifferentialTransaction::TYPE_ACTION:
return null;
case DifferentialTransaction::TYPE_INLINE:
return null;
case DifferentialTransaction::TYPE_UPDATE:
if ($this->getIsNewObject()) {
return null;
} else {
return $object->getActiveDiff()->getPHID();
}
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case DifferentialTransaction::TYPE_ACTION:
case DifferentialTransaction::TYPE_UPDATE:
return $xaction->getNewValue();
case DifferentialTransaction::TYPE_INLINE:
return null;
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$actor_phid = $this->getActingAsPHID();
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return $xaction->hasComment();
case DifferentialTransaction::TYPE_ACTION:
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
$status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED;
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
$status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
$status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED;
$action_type = $xaction->getNewValue();
switch ($action_type) {
case DifferentialAction::ACTION_ACCEPT:
case DifferentialAction::ACTION_REJECT:
if ($action_type == DifferentialAction::ACTION_ACCEPT) {
$new_status = DifferentialReviewerStatus::STATUS_ACCEPTED;
} else {
$new_status = DifferentialReviewerStatus::STATUS_REJECTED;
}
$actor = $this->getActor();
// These transactions can cause effects in two ways: by altering the
// status of an existing reviewer; or by adding the actor as a new
// reviewer.
$will_add_reviewer = true;
foreach ($object->getReviewerStatus() as $reviewer) {
if ($reviewer->hasAuthority($actor)) {
if ($reviewer->getStatus() != $new_status) {
return true;
}
}
if ($reviewer->getReviewerPHID() == $actor_phid) {
$will_add_reviwer = false;
}
}
return $will_add_reviewer;
case DifferentialAction::ACTION_CLOSE:
return ($object->getStatus() != $status_closed);
case DifferentialAction::ACTION_ABANDON:
return ($object->getStatus() != $status_abandoned);
case DifferentialAction::ACTION_RECLAIM:
return ($object->getStatus() == $status_abandoned);
case DifferentialAction::ACTION_REOPEN:
return ($object->getStatus() == $status_closed);
case DifferentialAction::ACTION_RETHINK:
return ($object->getStatus() != $status_plan);
case DifferentialAction::ACTION_REQUEST:
return ($object->getStatus() != $status_review);
case DifferentialAction::ACTION_RESIGN:
foreach ($object->getReviewerStatus() as $reviewer) {
if ($reviewer->getReviewerPHID() == $actor_phid) {
return true;
}
}
return false;
case DifferentialAction::ACTION_CLAIM:
return ($actor_phid != $object->getAuthorPHID());
}
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
$status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
$status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED;
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$object->setViewPolicy($xaction->getNewValue());
return;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$object->setEditPolicy($xaction->getNewValue());
return;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_COMMENT:
case DifferentialTransaction::TYPE_INLINE:
return;
case PhabricatorTransactions::TYPE_EDGE:
return;
case DifferentialTransaction::TYPE_UPDATE:
if (!$this->getIsCloseByCommit() &&
(($object->getStatus() == $status_revision) ||
($object->getStatus() == $status_plan))) {
$object->setStatus($status_review);
}
$diff = $this->requireDiff($xaction->getNewValue());
$object->setLineCount($diff->getLineCount());
- $object->setRepositoryPHID($diff->getRepositoryPHID());
+ if ($this->repositoryPHIDOverride !== false) {
+ $object->setRepositoryPHID($this->repositoryPHIDOverride);
+ } else {
+ $object->setRepositoryPHID($diff->getRepositoryPHID());
+ }
$object->setArcanistProjectPHID($diff->getArcanistProjectPHID());
$object->attachActiveDiff($diff);
// TODO: Update the `diffPHID` once we add that.
return;
case DifferentialTransaction::TYPE_ACTION:
switch ($xaction->getNewValue()) {
case DifferentialAction::ACTION_RESIGN:
case DifferentialAction::ACTION_ACCEPT:
case DifferentialAction::ACTION_REJECT:
// These have no direct effects, and affect review status only
// indirectly by altering reviewers with TYPE_EDGE transactions.
return;
case DifferentialAction::ACTION_ABANDON:
$object->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED);
return;
case DifferentialAction::ACTION_RETHINK:
$object->setStatus($status_plan);
return;
case DifferentialAction::ACTION_RECLAIM:
$object->setStatus($status_review);
return;
case DifferentialAction::ACTION_REOPEN:
$object->setStatus($status_review);
return;
case DifferentialAction::ACTION_REQUEST:
$object->setStatus($status_review);
return;
case DifferentialAction::ACTION_CLOSE:
$object->setStatus(ArcanistDifferentialRevisionStatus::CLOSED);
return;
case DifferentialAction::ACTION_CLAIM:
$object->setAuthorPHID($this->getActingAsPHID());
return;
}
break;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
$actor = $this->getActor();
$actor_phid = $this->getActingAsPHID();
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED;
$edge_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER;
$edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST;
$is_sticky_accept = PhabricatorEnv::getEnvConfig(
'differential.sticky-accept');
$downgrade_rejects = false;
$downgrade_accepts = false;
if ($this->getIsCloseByCommit()) {
// Never downgrade reviewers when we're closing a revision after a
// commit.
} else {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
$downgrade_rejects = true;
if (!$is_sticky_accept) {
// If "sticky accept" is disabled, also downgrade the accepts.
$downgrade_accepts = true;
}
break;
case DifferentialTransaction::TYPE_ACTION:
switch ($xaction->getNewValue()) {
case DifferentialAction::ACTION_REQUEST:
$downgrade_rejects = true;
if ((!$is_sticky_accept) ||
($object->getStatus() != $status_plan)) {
// If the old state isn't "changes planned", downgrade the
// accepts. This exception allows an accepted revision to
// go through Plan Changes -> Request Review to return to
// "accepted" if the author didn't update the revision.
$downgrade_accepts = true;
}
break;
}
break;
}
}
$new_accept = DifferentialReviewerStatus::STATUS_ACCEPTED;
$new_reject = DifferentialReviewerStatus::STATUS_REJECTED;
$old_accept = DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER;
$old_reject = DifferentialReviewerStatus::STATUS_REJECTED_OLDER;
if ($downgrade_rejects || $downgrade_accepts) {
// When a revision is updated, change all "reject" to "rejected older
// revision". This means we won't immediately push the update back into
// "needs review", but outstanding rejects will still block it from
// moving to "accepted".
// We also do this for "Request Review", even though the diff is not
// updated directly. Essentially, this acts like an update which doesn't
// actually change the diff text.
$edits = array();
foreach ($object->getReviewerStatus() as $reviewer) {
if ($downgrade_rejects) {
if ($reviewer->getStatus() == $new_reject) {
$edits[$reviewer->getReviewerPHID()] = array(
'data' => array(
'status' => $old_reject,
),
);
}
}
if ($downgrade_accepts) {
if ($reviewer->getStatus() == $new_accept) {
$edits[$reviewer->getReviewerPHID()] = array(
'data' => array(
'status' => $old_accept,
),
);
}
}
}
if ($edits) {
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_reviewer)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => $edits));
}
}
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
if ($this->getIsCloseByCommit()) {
// Don't bother with any of this if this update is a side effect of
// commit detection.
break;
}
// When a revision is updated and the diff comes from a branch named
// "T123" or similar, automatically associate the commit with the
// task that the branch names.
$maniphest = 'PhabricatorManiphestApplication';
if (PhabricatorApplication::isClassInstalled($maniphest)) {
$diff = $this->requireDiff($xaction->getNewValue());
$branch = $diff->getBranch();
// No "$", to allow for branches like T123_demo.
$match = null;
if (preg_match('/^T(\d+)/i', $branch, $match)) {
$task_id = $match[1];
$tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withIDs(array($task_id))
->execute();
if ($tasks) {
$task = head($tasks);
$task_phid = $task->getPHID();
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_ref_task)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => array($task_phid => $task_phid)));
}
}
}
break;
case PhabricatorTransactions::TYPE_COMMENT:
// When a user leaves a comment, upgrade their reviewer status from
// "added" to "commented" if they're also a reviewer. We may further
// upgrade this based on other actions in the transaction group.
$status_added = DifferentialReviewerStatus::STATUS_ADDED;
$status_commented = DifferentialReviewerStatus::STATUS_COMMENTED;
$data = array(
'status' => $status_commented,
);
$edits = array();
foreach ($object->getReviewerStatus() as $reviewer) {
if ($reviewer->getReviewerPHID() == $actor_phid) {
if ($reviewer->getStatus() == $status_added) {
$edits[$actor_phid] = array(
'data' => $data,
);
}
}
}
if ($edits) {
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_reviewer)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => $edits));
}
break;
case DifferentialTransaction::TYPE_ACTION:
$action_type = $xaction->getNewValue();
switch ($action_type) {
case DifferentialAction::ACTION_ACCEPT:
case DifferentialAction::ACTION_REJECT:
if ($action_type == DifferentialAction::ACTION_ACCEPT) {
$data = array(
'status' => DifferentialReviewerStatus::STATUS_ACCEPTED,
);
} else {
$data = array(
'status' => DifferentialReviewerStatus::STATUS_REJECTED,
);
}
$edits = array();
foreach ($object->getReviewerStatus() as $reviewer) {
if ($reviewer->hasAuthority($actor)) {
$edits[$reviewer->getReviewerPHID()] = array(
'data' => $data,
);
}
}
// Also either update or add the actor themselves as a reviewer.
$edits[$actor_phid] = array(
'data' => $data,
);
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_reviewer)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => $edits));
break;
case DifferentialAction::ACTION_CLAIM:
// If the user is commandeering, add the previous owner as a
// reviewer and remove the actor.
$edits = array(
'-' => array(
$actor_phid => $actor_phid,
),
);
$owner_phid = $object->getAuthorPHID();
if ($owner_phid) {
$reviewer = new DifferentialReviewer(
$owner_phid,
array(
'status' => DifferentialReviewerStatus::STATUS_ADDED,
));
$edits['+'] = array(
$owner_phid => array(
'data' => $reviewer->getEdgeData(),
),
);
}
// NOTE: We're setting setIsCommandeerSideEffect() on this because
// normally you can't add a revision's author as a reviewer, but
// this action swaps them after validation executes.
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_reviewer)
->setIgnoreOnNoEffect(true)
->setIsCommandeerSideEffect(true)
->setNewValue($edits);
break;
case DifferentialAction::ACTION_RESIGN:
// If the user is resigning, add a separate reviewer edit
// transaction which removes them as a reviewer.
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_reviewer)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'-' => array(
$actor_phid => $actor_phid,
),
));
break;
}
break;
}
return $results;
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_COMMENT:
case DifferentialTransaction::TYPE_ACTION:
case DifferentialTransaction::TYPE_INLINE:
return;
case DifferentialTransaction::TYPE_UPDATE:
// Now that we're inside the transaction, do a final check.
$diff = $this->requireDiff($xaction->getNewValue());
// TODO: It would be slightly cleaner to just revalidate this
// transaction somehow using the same validation code, but that's
// not easy to do at the moment.
$revision_id = $diff->getRevisionID();
if ($revision_id && ($revision_id != $object->getID())) {
throw new Exception(
pht(
'Diff is already attached to another revision. You lost '.
'a race?'));
}
$diff->setRevisionID($object->getID());
$diff->save();
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function mergeEdgeData($type, array $u, array $v) {
$result = parent::mergeEdgeData($type, $u, $v);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER:
// When the same reviewer has their status updated by multiple
// transactions, we want the strongest status to win. An example of
// this is when a user adds a comment and also accepts a revision which
// they are a reviewer on. The comment creates a "commented" status,
// while the accept creates an "accepted" status. Since accept is
// stronger, it should win and persist.
$u_status = idx($u, 'status');
$v_status = idx($v, 'status');
$u_str = DifferentialReviewerStatus::getStatusStrength($u_status);
$v_str = DifferentialReviewerStatus::getStatusStrength($v_status);
if ($u_str > $v_str) {
$result['status'] = $u_status;
} else {
$result['status'] = $v_status;
}
break;
}
return $result;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// Load the most up-to-date version of the revision and its reviewers,
// so we don't need to try to deduce the state of reviewers by examining
// all the changes made by the transactions. Then, update the reviewers
// on the object to make sure we're acting on the current reviewer set
// (and, for example, sending mail to the right people).
$new_revision = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewerStatus(true)
->needActiveDiffs(true)
->withIDs(array($object->getID()))
->executeOne();
if (!$new_revision) {
throw new Exception(
pht('Failed to load revision from transaction finalization.'));
}
$object->attachReviewerStatus($new_revision->getReviewerStatus());
$object->attachActiveDiff($new_revision->getActiveDiff());
$object->attachRepository($new_revision->getRepository());
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
$diff = $this->requireDiff($xaction->getNewValue(), true);
// Update these denormalized index tables when we attach a new
// diff to a revision.
$this->updateRevisionHashTable($object, $diff);
$this->updateAffectedPathTable($object, $diff);
break;
}
}
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
$status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
$old_status = $object->getStatus();
switch ($old_status) {
case $status_accepted:
case $status_revision:
case $status_review:
// Try to move a revision to "accepted". We look for:
//
// - at least one accepting reviewer who is a user; and
// - no rejects; and
// - no rejects of older diffs; and
// - no blocking reviewers.
$has_accepting_user = false;
$has_rejecting_reviewer = false;
$has_rejecting_older_reviewer = false;
$has_blocking_reviewer = false;
foreach ($object->getReviewerStatus() as $reviewer) {
$reviewer_status = $reviewer->getStatus();
switch ($reviewer_status) {
case DifferentialReviewerStatus::STATUS_REJECTED:
$has_rejecting_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_REJECTED_OLDER:
$has_rejecting_older_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_BLOCKING:
$has_blocking_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_ACCEPTED:
if ($reviewer->isUser()) {
$has_accepting_user = true;
}
break;
}
}
$new_status = null;
if ($has_accepting_user &&
!$has_rejecting_reviewer &&
!$has_rejecting_older_reviewer &&
!$has_blocking_reviewer) {
$new_status = $status_accepted;
} else if ($has_rejecting_reviewer) {
// This isn't accepted, and there's at least one rejecting reviewer,
// so the revision needs changes. This usually happens after a
// "reject".
$new_status = $status_revision;
} else if ($old_status == $status_accepted) {
// This revision was accepted, but it no longer satisfies the
// conditions for acceptance. This usually happens after an accepting
// reviewer resigns or is removed.
$new_status = $status_review;
}
if ($new_status !== null && ($new_status != $old_status)) {
$xaction = id(new DifferentialTransaction())
->setTransactionType(DifferentialTransaction::TYPE_STATUS)
->setOldValue($old_status)
->setNewValue($new_status);
$xaction = $this->populateTransaction($object, $xaction)->save();
$xactions[] = $xaction;
$object->setStatus($new_status)->save();
}
break;
default:
// Revisions can't transition out of other statuses (like closed or
// abandoned) as a side effect of reviewer status changes.
break;
}
return $xactions;
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
$config_self_accept_key = 'differential.allow-self-accept';
$allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);
foreach ($xactions as $xaction) {
switch ($type) {
case PhabricatorTransactions::TYPE_EDGE:
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER:
// Prevent the author from becoming a reviewer.
// NOTE: This is pretty gross, but this restriction is unusual.
// If we end up with too much more of this, we should try to clean
// this up -- maybe by moving validation to after transactions
// are adjusted (so we can just examine the final value) or adding
// a second phase there?
$author_phid = $object->getAuthorPHID();
$new = $xaction->getNewValue();
$add = idx($new, '+', array());
$eq = idx($new, '=', array());
$phids = array_keys($add + $eq);
foreach ($phids as $phid) {
if (($phid == $author_phid) &&
!$allow_self_accept &&
!$xaction->getIsCommandeerSideEffect()) {
$errors[] =
new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('The author of a revision can not be a reviewer.'),
$xaction);
}
}
break;
}
break;
case DifferentialTransaction::TYPE_UPDATE:
$diff = $this->loadDiff($xaction->getNewValue());
if (!$diff) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('The specified diff does not exist.'),
$xaction);
} else if (($diff->getRevisionID()) &&
($diff->getRevisionID() != $object->getID())) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can not update this revision to the specified diff, '.
'because the diff is already attached to another revision.'),
$xaction);
}
break;
case DifferentialTransaction::TYPE_ACTION:
$error = $this->validateDifferentialAction(
$object,
$type,
$xaction,
$xaction->getNewValue());
if ($error) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$error,
$xaction);
}
break;
}
}
return $errors;
}
private function validateDifferentialAction(
DifferentialRevision $revision,
$type,
DifferentialTransaction $xaction,
$action) {
$author_phid = $revision->getAuthorPHID();
$actor_phid = $this->getActingAsPHID();
$actor_is_author = ($author_phid == $actor_phid);
$config_abandon_key = 'differential.always-allow-abandon';
$always_allow_abandon = PhabricatorEnv::getEnvConfig($config_abandon_key);
$config_close_key = 'differential.always-allow-close';
$always_allow_close = PhabricatorEnv::getEnvConfig($config_close_key);
$config_reopen_key = 'differential.allow-reopen';
$allow_reopen = PhabricatorEnv::getEnvConfig($config_reopen_key);
$config_self_accept_key = 'differential.allow-self-accept';
$allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);
$revision_status = $revision->getStatus();
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
$status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED;
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
switch ($action) {
case DifferentialAction::ACTION_ACCEPT:
if ($actor_is_author && !$allow_self_accept) {
return pht(
'You can not accept this revision because you are the owner.');
}
if ($revision_status == $status_abandoned) {
return pht(
'You can not accept this revision because it has been '.
'abandoned.');
}
if ($revision_status == $status_closed) {
return pht(
'You can not accept this revision because it has already been '.
'closed.');
}
// TODO: It would be nice to make this generic at some point.
$signatures = DifferentialRequiredSignaturesField::loadForRevision(
$revision);
foreach ($signatures as $phid => $signed) {
if (!$signed) {
return pht(
'You can not accept this revision because the author has '.
'not signed all of the required legal documents.');
}
}
break;
case DifferentialAction::ACTION_REJECT:
if ($actor_is_author) {
return pht(
'You can not request changes to your own revision.');
}
if ($revision_status == $status_abandoned) {
return pht(
'You can not request changes to this revision because it has been '.
'abandoned.');
}
if ($revision_status == $status_closed) {
return pht(
'You can not request changes to this revision because it has '.
'already been closed.');
}
break;
case DifferentialAction::ACTION_RESIGN:
// You can always resign from a revision if you're a reviewer. If you
// aren't, this is a no-op rather than invalid.
break;
case DifferentialAction::ACTION_CLAIM:
// You can claim a revision if you're not the owner. If you are, this
// is a no-op rather than invalid.
if ($revision_status == $status_closed) {
return pht(
'You can not commandeer this revision because it has already been '.
'closed.');
}
break;
case DifferentialAction::ACTION_ABANDON:
if (!$actor_is_author && !$always_allow_abandon) {
return pht(
'You can not abandon this revision because you do not own it. '.
'You can only abandon revisions you own.');
}
if ($revision_status == $status_closed) {
return pht(
'You can not abandon this revision because it has already been '.
'closed.');
}
// NOTE: Abandons of already-abandoned revisions are treated as no-op
// instead of invalid. Other abandons are OK.
break;
case DifferentialAction::ACTION_RECLAIM:
if (!$actor_is_author) {
return pht(
'You can not reclaim this revision because you do not own '.
'it. You can only reclaim revisions you own.');
}
if ($revision_status == $status_closed) {
return pht(
'You can not reclaim this revision because it has already been '.
'closed.');
}
// NOTE: Reclaims of other non-abandoned revisions are treated as no-op
// instead of invalid.
break;
case DifferentialAction::ACTION_REOPEN:
if (!$allow_reopen) {
return pht(
'The reopen action is not enabled on this Phabricator install. '.
'Adjust your configuration to enable it.');
}
// NOTE: If the revision is not closed, this is caught as a no-op
// instead of an invalid transaction.
break;
case DifferentialAction::ACTION_RETHINK:
if (!$actor_is_author) {
return pht(
'You can not plan changes to this revision because you do not '.
'own it. To plan changes to a revision, you must be its owner.');
}
switch ($revision_status) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
// These are OK.
break;
case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
// Let this through, it's a no-op.
break;
case ArcanistDifferentialRevisionStatus::ABANDONED:
return pht(
'You can not plan changes to this revision because it has '.
'been abandoned.');
case ArcanistDifferentialRevisionStatus::CLOSED:
return pht(
'You can not plan changes to this revision because it has '.
'already been closed.');
default:
throw new Exception(
pht(
'Encountered unexpected revision status ("%s") when '.
'validating "%s" action.',
$revision_status,
$action));
}
break;
case DifferentialAction::ACTION_REQUEST:
if (!$actor_is_author) {
return pht(
'You can not request review of this revision because you do '.
'not own it. To request review of a revision, you must be its '.
'owner.');
}
switch ($revision_status) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
// These are OK.
break;
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
// This will be caught as "no effect" later on.
break;
case ArcanistDifferentialRevisionStatus::ABANDONED:
return pht(
'You can not request review of this revision because it has '.
'been abandoned. Instead, reclaim it.');
case ArcanistDifferentialRevisionStatus::CLOSED:
return pht(
'You can not request review of this revision because it has '.
'already been closed.');
default:
throw new Exception(
pht(
'Encountered unexpected revision status ("%s") when '.
'validating "%s" action.',
$revision_status,
$action));
}
break;
case DifferentialAction::ACTION_CLOSE:
// We force revisions closed when we discover a corresponding commit.
// In this case, revisions are allowed to transition to closed from
// any state. This is an automated action taken by the daemons.
if (!$this->getIsCloseByCommit()) {
if (!$actor_is_author && !$always_allow_close) {
return pht(
'You can not close this revision because you do not own it. To '.
'close a revision, you must be its owner.');
}
if ($revision_status != $status_accepted) {
return pht(
'You can not close this revision because it has not been '.
'accepted. You can only close accepted revisions.');
}
}
break;
}
return null;
}
protected function sortTransactions(array $xactions) {
$xactions = parent::sortTransactions($xactions);
$head = array();
$tail = array();
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == DifferentialTransaction::TYPE_INLINE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {}
return parent::requireCapabilities($object, $xaction);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
$phids[] = $object->getAuthorPHID();
foreach ($object->getReviewerStatus() as $reviewer) {
$phids[] = $reviewer->getReviewerPHID();
}
return $phids;
}
protected function getMailCC(PhabricatorLiskDAO $object) {
$phids = parent::getMailCC($object);
if ($this->heraldEmailPHIDs) {
foreach ($this->heraldEmailPHIDs as $phid) {
$phids[] = $phid;
}
}
return $phids;
}
protected function getMailAction(
PhabricatorLiskDAO $object,
array $xactions) {
$action = parent::getMailAction($object, $xactions);
$strongest = $this->getStrongestAction($object, $xactions);
switch ($strongest->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
$count = new PhutilNumber($object->getLineCount());
$action = pht('%s, %s line(s)', $action, $count);
break;
}
return $action;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
// This is nonstandard, but retains threading with older messages.
$phid = $object->getPHID();
return "differential-rev-{$phid}-req";
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new DifferentialReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
$original_title = $object->getOriginalTitle();
$subject = "D{$id}: {$title}";
$thread_topic = "D{$id}: {$original_title}";
return id(new PhabricatorMetaMTAMail())
->setSubject($subject)
->addHeader('Thread-Topic', $thread_topic);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$type_inline = DifferentialTransaction::TYPE_INLINE;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction;
}
}
$changed_uri = $this->getChangedPriorToCommitURI();
if ($changed_uri) {
$body->addLinkSection(
pht('CHANGED PRIOR TO COMMIT'),
$changed_uri);
}
if ($inlines) {
$body->addTextSection(
pht('INLINE COMMENTS'),
$this->renderInlineCommentsForMail($object, $inlines));
}
$body->addLinkSection(
pht('REVISION DETAIL'),
PhabricatorEnv::getProductionURI('/D'.$object->getID()));
$update_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
$update_xaction = $xaction;
break;
}
}
if ($update_xaction) {
$diff = $this->requireDiff($update_xaction->getNewValue(), true);
$body->addTextSection(
pht('AFFECTED FILES'),
$this->renderAffectedFilesForMail($diff));
$config_key_inline = 'metamta.differential.inline-patches';
$config_inline = PhabricatorEnv::getEnvConfig($config_key_inline);
$config_key_attach = 'metamta.differential.attach-patches';
$config_attach = PhabricatorEnv::getEnvConfig($config_key_attach);
if ($config_inline || $config_attach) {
$patch_section = $this->renderPatchForMail($diff);
$lines = count(phutil_split_lines($patch_section->getPlaintext()));
if ($config_inline && ($lines <= $config_inline)) {
$body->addTextSection(
pht('CHANGE DETAILS'),
$patch_section);
}
if ($config_attach) {
$name = pht('D%s.%s.patch', $object->getID(), $diff->getID());
$mime_type = 'text/x-patch; charset=utf-8';
$body->addAttachment(
new PhabricatorMetaMTAAttachment(
$patch_section->getPlaintext(), $name, $mime_type));
}
}
}
return $body;
}
public function getMailTagsMap() {
return array(
MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST =>
pht('A revision is created.'),
MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED =>
pht('A revision is updated.'),
MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT =>
pht('Someone comments on a revision.'),
MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED =>
pht('A revision is closed.'),
MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS =>
pht("A revision's reviewers change."),
MetaMTANotificationType::TYPE_DIFFERENTIAL_CC =>
pht("A revision's CCs change."),
MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER =>
pht('Other revision activity not listed above occurs.'),
);
}
protected function supportsSearch() {
return true;
}
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {}
return parent::extractFilePHIDsFromCustomTransaction($object, $xaction);
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$blocks,
PhutilMarkupEngine $engine) {
$flat_blocks = array_mergev($blocks);
$huge_block = implode("\n\n", $flat_blocks);
$task_map = array();
$task_refs = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($huge_block);
foreach ($task_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$task_id = (int)trim($monogram, 'tT');
$task_map[$task_id] = true;
}
}
$rev_map = array();
$rev_refs = id(new DifferentialCustomFieldDependsOnParser())
->parseCorpus($huge_block);
foreach ($rev_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$rev_id = (int)trim($monogram, 'dD');
$rev_map[$rev_id] = true;
}
}
$edges = array();
if ($task_map) {
$tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withIDs(array_keys($task_map))
->execute();
if ($tasks) {
$phid_map = mpull($tasks, 'getPHID', 'getPHID');
$edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST;
$edges[$edge_related] = $phid_map;
$this->setUnmentionablePHIDMap($phid_map);
}
}
if ($rev_map) {
$revs = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->withIDs(array_keys($rev_map))
->execute();
$rev_phids = mpull($revs, 'getPHID', 'getPHID');
// NOTE: Skip any write attempts if a user cleverly implies a revision
// depends upon itself.
unset($rev_phids[$object->getPHID()]);
if ($revs) {
$edge_depends = PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV;
$edges[$edge_depends] = $rev_phids;
}
}
$result = array();
foreach ($edges as $type => $specs) {
$result[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $type)
->setNewValue(array('+' => $specs));
}
return $result;
}
protected function indentForMail(array $lines) {
$indented = array();
foreach ($lines as $line) {
$indented[] = '> '.$line;
}
return $indented;
}
protected function nestCommentHistory(
DifferentialTransactionComment $comment, array $comments_by_line_number,
array $users_by_phid) {
$nested = array();
$previous_comments = $comments_by_line_number[$comment->getChangesetID()]
[$comment->getLineNumber()];
foreach ($previous_comments as $previous_comment) {
if ($previous_comment->getID() >= $comment->getID())
break;
$nested = $this->indentForMail(
array_merge(
$nested,
explode("\n", $previous_comment->getContent())));
$user = idx($users_by_phid, $previous_comment->getAuthorPHID(), null);
if ($user) {
array_unshift($nested, pht('%s wrote:', $user->getUserName()));
}
}
$nested = array_merge($nested, explode("\n", $comment->getContent()));
return implode("\n", $nested);
}
private function renderInlineCommentsForMail(
PhabricatorLiskDAO $object,
array $inlines) {
$context_key = 'metamta.differential.unified-comment-context';
$show_context = PhabricatorEnv::getEnvConfig($context_key);
$changeset_ids = array();
$line_numbers_by_changeset = array();
foreach ($inlines as $inline) {
$id = $inline->getComment()->getChangesetID();
$changeset_ids[$id] = $id;
$line_numbers_by_changeset[$id][] =
$inline->getComment()->getLineNumber();
}
$changesets = id(new DifferentialChangesetQuery())
->setViewer($this->getActor())
->withIDs($changeset_ids)
->needHunks(true)
->execute();
$inline_groups = DifferentialTransactionComment::sortAndGroupInlines(
$inlines,
$changesets);
if ($show_context) {
$hunk_parser = new DifferentialHunkParser();
$table = new DifferentialTransactionComment();
$conn_r = $table->establishConnection('r');
$queries = array();
foreach ($line_numbers_by_changeset as $id => $line_numbers) {
$queries[] = qsprintf(
$conn_r,
'(changesetID = %d AND lineNumber IN (%Ld))',
$id, $line_numbers);
}
$all_comments = id(new DifferentialTransactionComment())->loadAllWhere(
'transactionPHID IS NOT NULL AND (%Q)', implode(' OR ', $queries));
$comments_by_line_number = array();
foreach ($all_comments as $comment) {
$comments_by_line_number
[$comment->getChangesetID()]
[$comment->getLineNumber()]
[$comment->getID()] = $comment;
}
$author_phids = mpull($all_comments, 'getAuthorPHID');
$authors = id(new PhabricatorPeopleQuery())
->setViewer($this->getActor())
->withPHIDs($author_phids)
->execute();
$authors_by_phid = mpull($authors, null, 'getPHID');
}
$section = new PhabricatorMetaMTAMailSection();
foreach ($inline_groups as $changeset_id => $group) {
$changeset = idx($changesets, $changeset_id);
if (!$changeset) {
continue;
}
foreach ($group as $inline) {
$comment = $inline->getComment();
$file = $changeset->getFilename();
$start = $comment->getLineNumber();
$len = $comment->getLineLength();
if ($len) {
$range = $start.'-'.($start + $len);
} else {
$range = $start;
}
$inline_content = $comment->getContent();
if (!$show_context) {
$section->addFragment("{$file}:{$range} {$inline_content}");
} else {
$patch = $hunk_parser->makeContextDiff(
$changeset->getHunks(),
$comment->getIsNewFile(),
$comment->getLineNumber(),
$comment->getLineLength(),
1);
$nested_comments = $this->nestCommentHistory(
$inline->getComment(), $comments_by_line_number, $authors_by_phid);
$section->addFragment('================')
->addFragment('Comment at: '.$file.':'.$range)
->addPlaintextFragment($patch)
->addHTMLFragment($this->renderPatchHTMLForMail($patch))
->addFragment('----------------')
->addFragment($nested_comments)
->addFragment(null);
}
}
}
return $section;
}
private function loadDiff($phid, $need_changesets = false) {
$query = id(new DifferentialDiffQuery())
->withPHIDs(array($phid))
->setViewer($this->getActor());
if ($need_changesets) {
$query->needChangesets(true);
}
return $query->executeOne();
}
private function requireDiff($phid, $need_changesets = false) {
$diff = $this->loadDiff($phid, $need_changesets);
if (!$diff) {
throw new Exception(pht('Diff "%s" does not exist!', $phid));
}
return $diff;
}
/* -( Herald Integration )------------------------------------------------- */
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
if ($this->getIsNewObject()) {
return true;
}
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
if (!$this->getIsCloseByCommit()) {
return true;
}
break;
case DifferentialTransaction::TYPE_ACTION:
switch ($xaction->getNewValue()) {
case DifferentialAction::ACTION_CLAIM:
// When users commandeer revisions, we may need to trigger
// signatures or author-based rules.
return true;
}
break;
}
}
return parent::shouldApplyHeraldRules($object, $xactions);
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
$unsubscribed_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER);
$subscribed_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
$revision = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->withPHIDs(array($object->getPHID()))
->needActiveDiffs(true)
->needReviewerStatus(true)
->executeOne();
if (!$revision) {
throw new Exception(
pht(
'Failed to load revision for Herald adapter construction!'));
}
$adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter(
$revision,
$revision->getActiveDiff());
$reviewers = $revision->getReviewerStatus();
$reviewer_phids = mpull($reviewers, 'getReviewerPHID');
$adapter->setExplicitCCs($subscribed_phids);
$adapter->setExplicitReviewers($reviewer_phids);
$adapter->setForbiddenCCs($unsubscribed_phids);
return $adapter;
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$xactions = array();
// Build a transaction to adjust CCs.
$ccs = array(
'+' => array_keys($adapter->getCCsAddedByHerald()),
'-' => array_keys($adapter->getCCsRemovedByHerald()),
);
$value = array();
foreach ($ccs as $type => $phids) {
foreach ($phids as $phid) {
$value[$type][$phid] = $phid;
}
}
if ($value) {
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue($value);
}
// Build a transaction to adjust reviewers.
$reviewers = array(
DifferentialReviewerStatus::STATUS_ADDED =>
array_keys($adapter->getReviewersAddedByHerald()),
DifferentialReviewerStatus::STATUS_BLOCKING =>
array_keys($adapter->getBlockingReviewersAddedByHerald()),
);
$old_reviewers = $object->getReviewerStatus();
$old_reviewers = mpull($old_reviewers, null, 'getReviewerPHID');
$value = array();
foreach ($reviewers as $status => $phids) {
foreach ($phids as $phid) {
if ($phid == $object->getAuthorPHID()) {
// Don't try to add the revision's author as a reviewer, since this
// isn't valid and doesn't make sense.
continue;
}
// If the target is already a reviewer, don't try to change anything
// if their current status is at least as strong as the new status.
// For example, don't downgrade an "Accepted" to a "Blocking Reviewer".
$old_reviewer = idx($old_reviewers, $phid);
if ($old_reviewer) {
$old_status = $old_reviewer->getStatus();
$old_strength = DifferentialReviewerStatus::getStatusStrength(
$old_status);
$new_strength = DifferentialReviewerStatus::getStatusStrength(
$status);
if ($new_strength <= $old_strength) {
continue;
}
}
$value['+'][$phid] = array(
'data' => array(
'status' => $status,
),
);
}
}
if ($value) {
$edge_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER;
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_reviewer)
->setNewValue($value);
}
// Require legalpad document signatures.
$legal_phids = $adapter->getRequiredSignatureDocumentPHIDs();
if ($legal_phids) {
// We only require signatures of documents which have not already
// been signed. In general, this reduces the amount of churn that
// signature rules cause.
$signatures = id(new LegalpadDocumentSignatureQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withDocumentPHIDs($legal_phids)
->withSignerPHIDs(array($object->getAuthorPHID()))
->execute();
$signed_phids = mpull($signatures, 'getDocumentPHID');
$legal_phids = array_diff($legal_phids, $signed_phids);
// If we still have something to trigger, add the edges.
if ($legal_phids) {
$edge_legal = PhabricatorEdgeConfig::TYPE_OBJECT_NEEDS_SIGNATURE;
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_legal)
->setNewValue(
array(
'+' => array_fuse($legal_phids),
));
}
}
// Save extra email PHIDs for later.
$email_phids = $adapter->getEmailPHIDsAddedByHerald();
$this->heraldEmailPHIDs = array_keys($email_phids);
// Apply build plans.
HarbormasterBuildable::applyBuildPlans(
$adapter->getDiff()->getPHID(),
$adapter->getPHID(),
$adapter->getBuildPlans());
return $xactions;
}
/**
* Update the table which links Differential revisions to paths they affect,
* so Diffusion can efficiently find pending revisions for a given file.
*/
private function updateAffectedPathTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$repository = $revision->getRepository();
if (!$repository) {
// The repository where the code lives is untracked.
return;
}
$path_prefix = null;
$local_root = $diff->getSourceControlPath();
if ($local_root) {
// We're in a working copy which supports subdirectory checkouts (e.g.,
// SVN) so we need to figure out what prefix we should add to each path
// (e.g., trunk/projects/example/) to get the absolute path from the
// root of the repository. DVCS systems like Git and Mercurial are not
// affected.
// Normalize both paths and check if the repository root is a prefix of
// the local root. If so, throw it away. Note that this correctly handles
// the case where the remote path is "/".
$local_root = id(new PhutilURI($local_root))->getPath();
$local_root = rtrim($local_root, '/');
$repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath();
$repo_root = rtrim($repo_root, '/');
if (!strncmp($repo_root, $local_root, strlen($repo_root))) {
$path_prefix = substr($local_root, strlen($repo_root));
}
}
$changesets = $diff->getChangesets();
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $path_prefix.'/'.$changeset->getFilename();
}
// Mark this as also touching all parent paths, so you can see all pending
// changes to any file within a directory.
$all_paths = array();
foreach ($paths as $local) {
foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) {
$all_paths[$path] = true;
}
}
$all_paths = array_keys($all_paths);
$path_ids =
PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
$all_paths);
$table = new DifferentialAffectedPath();
$conn_w = $table->establishConnection('w');
$sql = array();
foreach ($path_ids as $path_id) {
$sql[] = qsprintf(
$conn_w,
'(%d, %d, %d, %d)',
$repository->getID(),
$path_id,
time(),
$revision->getID());
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$table->getTableName(),
$revision->getID());
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q',
$table->getTableName(),
implode(', ', $chunk));
}
}
/**
* Update the table connecting revisions to DVCS local hashes, so we can
* identify revisions by commit/tree hashes.
*/
private function updateRevisionHashTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$vcs = $diff->getSourceControlSystem();
if ($vcs == DifferentialRevisionControlSystem::SVN) {
// Subversion has no local commit or tree hash information, so we don't
// have to do anything.
return;
}
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff->getID(),
'local:commits');
if (!$property) {
return;
}
$hashes = array();
$data = $property->getData();
switch ($vcs) {
case DifferentialRevisionControlSystem::GIT:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
$commit['commit'],
);
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
$commit['tree'],
);
}
break;
case DifferentialRevisionControlSystem::MERCURIAL:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
$commit['rev'],
);
}
break;
}
$conn_w = $revision->establishConnection('w');
$sql = array();
foreach ($hashes as $info) {
list($type, $hash) = $info;
$sql[] = qsprintf(
$conn_w,
'(%d, %s, %s)',
$revision->getID(),
$type,
$hash);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
ArcanistDifferentialRevisionHash::TABLE_NAME,
$revision->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (revisionID, type, hash) VALUES %Q',
ArcanistDifferentialRevisionHash::TABLE_NAME,
implode(', ', $sql));
}
}
private function renderAffectedFilesForMail(DifferentialDiff $diff) {
$changesets = $diff->getChangesets();
$filenames = mpull($changesets, 'getDisplayFilename');
sort($filenames);
$count = count($filenames);
$max = 250;
if ($count > $max) {
$filenames = array_slice($filenames, 0, $max);
$filenames[] = pht('(%d more files...)', ($count - $max));
}
return implode("\n", $filenames);
}
private function renderPatchHTMLForMail($patch) {
return phutil_tag('pre',
array('style' => 'font-family: monospace;'), $patch);
}
private function renderPatchForMail(DifferentialDiff $diff) {
$format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
$patch = id(new DifferentialRawDiffRenderer())
->setViewer($this->getActor())
->setFormat($format)
->setChangesets($diff->getChangesets())
->buildPatch();
$section = new PhabricatorMetaMTAMailSection();
$section->addHTMLFragment($this->renderPatchHTMLForMail($patch));
$section->addPlaintextFragment($patch);
return $section;
}
}