1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

DifferentialRevisionEditor

This commit is contained in:
epriestley 2011-01-25 17:17:19 -08:00
parent d37692ac06
commit de1fb8ac7d
24 changed files with 1448 additions and 44 deletions

View file

@ -64,6 +64,7 @@ phutil_register_library_map(array(
'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/find',
'ConduitException' => 'applications/conduit/protocol/exception',
'DifferentialAction' => 'applications/differential/constants/action',
'DifferentialCCWelcomeMail' => 'applications/differential/mail/ccwelcome',
'DifferentialChangeType' => 'applications/differential/constants/changetype',
'DifferentialChangeset' => 'applications/differential/storage/changeset',
'DifferentialChangesetDetailView' => 'applications/differential/view/changesetdetailview',
@ -73,14 +74,20 @@ phutil_register_library_map(array(
'DifferentialController' => 'applications/differential/controller/base',
'DifferentialDAO' => 'applications/differential/storage/base',
'DifferentialDiff' => 'applications/differential/storage/diff',
'DifferentialDiffContentMail' => 'applications/differential/mail/diffcontent',
'DifferentialDiffProperty' => 'applications/differential/storage/diffproperty',
'DifferentialDiffTableOfContentsView' => 'applications/differential/view/difftableofcontents',
'DifferentialDiffViewController' => 'applications/differential/controller/diffview',
'DifferentialFeedbackMail' => 'applications/differential/mail/feedback',
'DifferentialHunk' => 'applications/differential/storage/hunk',
'DifferentialLintStatus' => 'applications/differential/constants/lintstatus',
'DifferentialMail' => 'applications/differential/mail/base',
'DifferentialNewDiffMail' => 'applications/differential/mail/newdiff',
'DifferentialReviewRequestMail' => 'applications/differential/mail/reviewrequest',
'DifferentialRevision' => 'applications/differential/storage/revision',
'DifferentialRevisionControlSystem' => 'applications/differential/constants/revisioncontrolsystem',
'DifferentialRevisionEditController' => 'applications/differential/controller/revisionedit',
'DifferentialRevisionEditor' => 'applications/differential/editor/revision',
'DifferentialRevisionListController' => 'applications/differential/controller/revisionlist',
'DifferentialRevisionStatus' => 'applications/differential/constants/revisionstatus',
'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus',
@ -206,6 +213,7 @@ phutil_register_library_map(array(
'ConduitAPI_differential_setdiffproperty_Method' => 'ConduitAPIMethod',
'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod',
'ConduitAPI_user_find_Method' => 'ConduitAPIMethod',
'DifferentialCCWelcomeMail' => 'DifferentialReviewRequestMail',
'DifferentialChangeset' => 'DifferentialDAO',
'DifferentialChangesetDetailView' => 'AphrontView',
'DifferentialChangesetListView' => 'AphrontView',
@ -213,10 +221,14 @@ phutil_register_library_map(array(
'DifferentialController' => 'PhabricatorController',
'DifferentialDAO' => 'PhabricatorLiskDAO',
'DifferentialDiff' => 'DifferentialDAO',
'DifferentialDiffContentMail' => 'DifferentialMail',
'DifferentialDiffProperty' => 'DifferentialDAO',
'DifferentialDiffTableOfContentsView' => 'AphrontView',
'DifferentialDiffViewController' => 'DifferentialController',
'DifferentialFeedbackMail' => 'DifferentialMail',
'DifferentialHunk' => 'DifferentialDAO',
'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail',
'DifferentialReviewRequestMail' => 'DifferentialMail',
'DifferentialRevision' => 'DifferentialDAO',
'DifferentialRevisionEditController' => 'DifferentialController',
'DifferentialRevisionListController' => 'DifferentialController',

View file

@ -19,6 +19,10 @@
class PhabricatorConduitAPIController
extends PhabricatorConduitController {
public function shouldRequireLogin() {
return false;
}
private $method;
public function willProcessRequest(array $data) {

View file

@ -41,6 +41,8 @@ class DifferentialDiffViewController extends DifferentialController {
$action_form = new AphrontFormView();
$action_form
->setAction('/differential/revision/edit/')
->addHiddenInput('diffID', $diff->getID())
->addHiddenInput('viaDiffView', 1)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Attach To')

View file

@ -34,25 +34,80 @@ class DifferentialRevisionEditController extends DifferentialController {
} else {
$revision = new DifferentialRevision();
}
/*
$e_name = true;
$errors = array();
$request = $this->getRequest();
if ($request->isFormPost()) {
$category->setName($request->getStr('name'));
$category->setSequence($request->getStr('sequence'));
$diff_id = $request->getInt('diffID');
if ($diff_id) {
$diff = id(new DifferentialDiff())->load($diff_id);
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 (!strlen($category->getName())) {
$errors[] = 'Category name is required.';
$e_name = 'Required';
$e_title = true;
$e_testplan = true;
$errors = array();
if ($request->isFormPost() && !$request->getStr('viaDiffView')) {
$revision->setTitle($request->getStr('title'));
$revision->setSummary($request->getStr('summary'));
$revision->setTestPlan($request->getStr('testplan'));
$revision->setBlameRevision($request->getStr('blame'));
$revision->setRevertPlan($request->getStr('revert'));
if (!strlen(trim($revision->getTitle()))) {
$errors[] = 'You must provide a title.';
$e_title = 'Required';
}
if (!strlen(trim($revision->getTestPlan()))) {
$errors[] = 'You must provide a test plan.';
$e_testplan = 'Required';
}
$user_phid = $request->getUser()->getPHID();
if (in_array($user_phid, $request->getArr('reviewers'))) {
$errors[] = 'You may not review your own revision.';
}
if (!$errors) {
$category->save();
return id(new AphrontRedirectResponse())
->setURI('/directory/category/');
$editor = new DifferentialRevisionEditor($revision, $user_phid);
if ($diff) {
$editor->addDiff($diff, $request->getStr('comments'));
}
$editor->setCCPHIDs($request->getArr('cc'));
$editor->setReviewers($request->getArr('reviewers'));
$editor->save();
$response = id(new AphrontRedirectResponse())
->setURI('/D'.$revision->getID());
}
$reviewer_phids = $request->getArr('reviewers');
$cc_phids = $request->getArr('cc');
} else {
// $reviewer_phids = $revision->getReviewers();
// $cc_phids = $revision->getCCPHIDs();
$reviewer_phids = array();
$cc_phids = array();
}
$form = new AphrontFormView();
if ($diff) {
$form->addHiddenInput('diffID', $diff->getID());
}
if ($revision->getID()) {
$form->setAction('/differential/revision/edit/'.$revision->getID().'/');
} else {
$form->setAction('/differential/revision/edit/');
}
$error_view = null;
@ -61,29 +116,15 @@ class DifferentialRevisionEditController extends DifferentialController {
->setTitle('Form Errors')
->setErrors($errors);
}
*/
$e_name = true;
$e_testplan = true;
$form = new AphrontFormView();
if ($revision->getID()) {
$form->setAction('/differential/revision/edit/'.$revision->getID().'/');
} else {
$form->setAction('/differential/revision/edit/');
}
$reviewer_map = array(
1 => 'A Zebra',
2 => 'Pie Messenger',
);
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Name')
->setName('name')
->setValue($revision->getName())
->setError($e_name))
->setLabel('Title')
->setName('title')
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($revision->getTitle())
->setError($e_title))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Summary')
@ -99,13 +140,13 @@ class DifferentialRevisionEditController extends DifferentialController {
id(new AphrontFormTokenizerControl())
->setLabel('Reviewers')
->setName('reviewers')
->setDatasource('/typeahead/common/user/')
->setDatasource('/typeahead/common/users/')
->setValue($reviewer_map))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('CC')
->setName('cc')
->setDatasource('/typeahead/common/user/')
->setDatasource('/typeahead/common/mailable/')
->setValue($reviewer_map))
->appendChild(
id(new AphrontFormTextControl())
@ -116,13 +157,20 @@ class DifferentialRevisionEditController extends DifferentialController {
'change fixes.'))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Revert')
->setLabel('Revert Plan')
->setName('revert')
->setValue($revision->getRevertPlan())
->setCaption('Special steps required to safely revert this change.'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save'));
->setCaption('Special steps required to safely revert this change.'));
$submit = id(new AphrontFormSubmitControl())
->setValue('Save');
if ($diff) {
$submit->addCancelButton('/differential/diff/'.$diff->getID().'/');
} else {
$submit->addCancelButton('/D'.$revision->getID());
}
$form->appendChild($submit);
$panel = new AphrontPanelView();
if ($revision->getID()) {
@ -134,7 +182,6 @@ class DifferentialRevisionEditController extends DifferentialController {
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$error_view = null;
return $this->buildStandardPageResponse(
array($error_view, $panel),
array(

View file

@ -7,10 +7,15 @@
phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/differential/controller/base');
phutil_require_module('phabricator', 'applications/differential/editor/revision');
phutil_require_module('phabricator', 'applications/differential/storage/diff');
phutil_require_module('phabricator', 'applications/differential/storage/revision');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'view/form/control/textarea');
phutil_require_module('phabricator', 'view/form/error');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phutil', 'utils');

View file

@ -0,0 +1,550 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Handle major edit operations to DifferentialRevision -- adding and removing
* reviewers, diffs, and CCs. Unlike simple edits, these changes trigger
* complicated email workflows.
*/
class DifferentialRevisionEditor {
protected $revision;
protected $actorPHID;
protected $cc = null;
protected $reviewers = null;
protected $diff;
protected $comments;
protected $silentUpdate;
public function __construct(DifferentialRevision $revision, $actor_phid) {
$this->revision = $revision;
$this->actorPHID = $actor_phid;
}
/*
public static function newRevisionFromRawMessageWithDiff(
DifferentialRawMessage $message,
Diff $diff,
$user) {
if ($message->getRevisionID()) {
throw new Exception(
"The provided commit message is already associated with a ".
"Differential revision.");
}
if ($message->getReviewedByNames()) {
throw new Exception(
"The provided commit message contains a 'Reviewed By:' field.");
}
$revision = new DifferentialRevision();
$revision->setPHID($revision->generatePHID());
$revision->setOwnerID($user);
$revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
$revision->attachReviewers(array());
$revision->attachCCPHIDs(array());
$editor = new DifferentialRevisionEditor($revision, $user);
self::copyFields($editor, $revision, $message, $user);
$editor->addDiff($diff, null);
$editor->save();
return $revision;
}
public static function newRevisionFromConduitWithDiff(
array $fields,
Diff $diff,
$user) {
$revision = new DifferentialRevision();
$revision->setPHID($revision->generatePHID());
$revision->setOwnerID($user);
$revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
$revision->attachReviewers(array());
$revision->attachCCPHIDs(array());
$editor = new DifferentialRevisionEditor($revision, $user);
$editor->copyFieldFromConduit($fields);
$editor->addDiff($diff, null);
$editor->save();
return $revision;
}
public static function copyFields(
DifferentialRevisionEditor $editor,
DifferentialRevision $revision,
DifferentialRawMessage $message,
$user) {
$revision->setName($message->getTitle());
$revision->setSummary($message->getSummary());
$revision->setTestPlan($message->getTestPlan());
$revision->setSVNBlameRevision($message->getBlameRevision());
$revision->setRevert($message->getRevertPlan());
$revision->setPlatformImpact($message->getPlatformImpact());
$revision->setBugzillaID($message->getBugzillaID());
$editor->setReviewers($message->getReviewerPHIDs());
$editor->setCCPHIDs($message->getCCPHIDs());
}
public function copyFieldFromConduit(array $fields) {
$user = $this->actorPHID;
$revision = $this->revision;
$revision->setName($fields['title']);
$revision->setSummary($fields['summary']);
$revision->setTestPlan($fields['testPlan']);
$revision->setSVNBlameRevision($fields['blameRevision']);
$revision->setRevert($fields['revertPlan']);
$revision->setPlatformImpact($fields['platformImpact']);
$revision->setBugzillaID($fields['bugzillaID']);
$this->setReviewers($fields['reviewerGUIDs']);
$this->setCCPHIDs($fields['ccGUIDs']);
}
*/
public function getRevision() {
return $this->revision;
}
public function setReviewers(array $reviewers) {
$this->reviewers = $reviewers;
return $this;
}
public function setCCPHIDs(array $cc) {
$this->cc = $cc;
return $this;
}
public function addDiff(DifferentialDiff $diff, $comments) {
if ($diff->getRevisionID() &&
$diff->getRevisionID() != $this->getRevision()->getID()) {
$diff_id = (int)$diff->getID();
$targ_id = (int)$this->getRevision()->getID();
$real_id = (int)$diff->getRevisionID();
throw new Exception(
"Can not attach diff #{$diff_id} to Revision D{$targ_id}, it is ".
"already attached to D{$real_id}.");
}
$this->diff = $diff;
$this->comments = $comments;
return $this;
}
protected function getDiff() {
return $this->diff;
}
protected function getComments() {
return $this->comments;
}
protected function getActorPHID() {
return $this->actorPHID;
}
public function isNewRevision() {
return !$this->getRevision()->getID();
}
/**
* A silent update does not trigger Herald rules or send emails. This is used
* for auto-amends at commit time.
*/
public function setSilentUpdate($silent) {
$this->silentUpdate = $silent;
return $this;
}
public function save() {
$revision = $this->getRevision();
// TODO
// $revision->openTransaction();
$is_new = $this->isNewRevision();
if ($is_new) {
// These fields aren't nullable; set them to sensible defaults if they
// haven't been configured. We're just doing this so we can generate an
// ID for the revision if we don't have one already.
$revision->setLineCount(0);
if ($revision->getStatus() === null) {
$revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
}
if ($revision->getTitle() === null) {
$revision->setTitle('Untitled Revision');
}
if ($revision->getOwnerPHID() === null) {
$revision->setOwnerPHID($this->getActorPHID());
}
$revision->save();
}
$revision->loadRelationships();
if ($this->reviewers === null) {
$this->reviewers = $revision->getReviewers();
}
if ($this->cc === null) {
$this->cc = $revision->getCCPHIDs();
}
// We're going to build up three dictionaries: $add, $rem, and $stable. The
// $add dictionary has added reviewers/CCs. The $rem dictionary has
// reviewers/CCs who have been removed, and the $stable array is
// reviewers/CCs who haven't changed. We're going to send new reviewers/CCs
// a different ("welcome") email than we send stable reviewers/CCs.
$old = array(
'rev' => array_fill_keys($revision->getReviewers(), true),
'ccs' => array_fill_keys($revision->getCCPHIDs(), true),
);
$diff = $this->getDiff();
$xscript_header = null;
$xscript_uri = null;
$new = array(
'rev' => array_fill_keys($this->reviewers, true),
'ccs' => array_fill_keys($this->cc, true),
);
$rem_ccs = array();
if ($diff) {
$diff->setRevisionID($revision->getID());
$revision->setLineCount($diff->getLineCount());
// TODO!
// $revision->setRepositoryID($diff->getRepositoryID());
/*
$iface = new DifferentialRevisionHeraldable($revision);
$iface->setExplicitCCs($new['ccs']);
$iface->setExplicitReviewers($new['rev']);
$iface->setForbiddenCCs($revision->getForbiddenCCPHIDs());
$iface->setForbiddenReviewers($revision->getForbiddenReviewers());
$iface->setDiff($diff);
$xscript = HeraldEngine::loadAndApplyRules($iface);
$xscript_uri = $xscript->getURI();
$xscript_phid = $xscript->getPHID();
$xscript_header = $xscript->getXHeraldRulesHeader();
$sub = array(
'rev' => array(),
'ccs' => $iface->getCCsAddedByHerald(),
);
$rem_ccs = $iface->getCCsRemovedByHerald();
*/
// TODO!
$sub = array(
'rev' => array(),
'ccs' => array(),
);
} else {
$sub = array(
'rev' => array(),
'ccs' => array(),
);
}
// Remove any CCs which are prevented by Herald rules.
$sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs);
$new['ccs'] = array_diff_key($new['ccs'], $rem_ccs);
$add = array();
$rem = array();
$stable = array();
foreach (array('rev', 'ccs') as $key) {
$add[$key] = array();
if ($new[$key] !== null) {
$add[$key] += array_diff_key($new[$key], $old[$key]);
}
$add[$key] += array_diff_key($sub[$key], $old[$key]);
$combined = $sub[$key];
if ($new[$key] !== null) {
$combined += $new[$key];
}
$rem[$key] = array_diff_key($old[$key], $combined);
$stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]);
}
self::removeReviewers(
$revision,
array_keys($rem['rev']),
$this->actorPHID);
self::addReviewers(
$revision,
array_keys($add['rev']),
$this->actorPHID);
// Add the owner to the relevant set of users so they get a copy of the
// email.
if (!$this->silentUpdate) {
if ($is_new) {
$add['rev'][$this->getActorPHID()] = true;
} else {
$stable['rev'][$this->getActorPHID()] = true;
}
}
$mail = array();
$changesets = null;
$feedback = null;
if ($diff) {
$changesets = $diff->loadChangesets();
// TODO: move to DifferentialFeedbackEditor
if (!$is_new) {
// TODO
// $feedback = $this->createFeedback();
}
if ($feedback) {
$mail[] = id(new DifferentialNewDiffMail(
$revision,
$this->getActorPHID(),
$changesets))
->setIsFirstMailAboutRevision($is_new)
->setIsFirstMailToRecipients($is_new)
->setComments($this->getComments())
->setToPHIDs(array_keys($stable['rev']))
->setCCPHIDs(array_keys($stable['ccs']));
}
// Save the changes we made above.
// TODO
// $diff->setDescription(substr($this->getComments(), 0, 80));
$diff->save();
// An updated diff should require review, as long as it's not committed
// or accepted. The "accepted" status is "sticky" to encourage courtesy
// re-diffs after someone accepts with minor changes/suggestions.
$status = $revision->getStatus();
if ($status != DifferentialRevisionStatus::COMMITTED &&
$status != DifferentialRevisionStatus::ACCEPTED) {
$revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
}
} else {
$diff = $revision->getActiveDiff();
if ($diff) {
$changesets = id(new DifferentialChangeset())->loadAllWithDiff($diff);
} else {
$changesets = array();
}
}
$revision->save();
// TODO
// $revision->saveTransaction();
$event = array(
'revision_id' => $revision->getID(),
'PHID' => $revision->getPHID(),
'action' => $is_new ? 'create' : 'update',
'actor' => $this->getActorPHID(),
);
// TODO
// id(new ToolsTimelineEvent('difx', fb_json_encode($event)))->record();
if ($this->silentUpdate) {
return;
}
// TODO
// $revision->attachReviewers(array_keys($new['rev']));
// $revision->attachCCPHIDs(array_keys($new['ccs']));
if ($add['ccs'] || $rem['ccs']) {
foreach (array_keys($add['ccs']) as $id) {
if (empty($new['ccs'][$id])) {
$reason_phid = 'TODO';//$xscript_phid;
} else {
$reason_phid = $this->getActorPHID();
}
self::addCCPHID($revision, $id, $reason_phid);
}
foreach (array_keys($rem['ccs']) as $id) {
if (empty($new['ccs'][$id])) {
$reason_phid = $this->getActorPHID();
} else {
$reason_phid = 'TODO';//$xscript_phid;
}
self::removeCCPHID($revision, $id, $reason_phid);
}
}
if ($add['rev']) {
$message = id(new DifferentialNewDiffMail(
$revision,
$this->getActorPHID(),
$changesets))
->setIsFirstMailAboutRevision($is_new)
->setIsFirstMailToRecipients(true)
->setToPHIDs(array_keys($add['rev']));
if ($is_new) {
// The first time we send an email about a revision, put the CCs in
// the "CC:" field of the same "Review Requested" email that reviewers
// get, so you don't get two initial emails if you're on a list that
// is CC'd.
$message->setCCPHIDs(array_keys($add['ccs']));
}
$mail[] = $message;
}
// If you were added as a reviewer and a CC, just give you the reviewer
// email. We could go to greater lengths to prevent this, but there's
// bunch of stuff with list subscriptions anyway. You can still get two
// emails, but only if a revision is updated and you are added as a reviewer
// at the same time a list you are on is added as a CC, which is rare and
// reasonable.
$add['ccs'] = array_diff_key($add['ccs'], $add['rev']);
if (!$is_new && $add['ccs']) {
$mail[] = id(new DifferentialCCWelcomeMail(
$revision,
$this->getActorPHID(),
$changesets))
->setIsFirstMailToRecipients(true)
->setToPHIDs(array_keys($add['ccs']));
}
foreach ($mail as $message) {
// TODO
// $message->setHeraldTranscriptURI($xscript_uri);
// $message->setXHeraldRulesHeader($xscript_header);
$message->send();
}
}
public function addCCPHID(
DifferentialRevision $revision,
$phid,
$reason_phid) {
self::alterCCPHID($revision, $phid, true, $reason_phid);
}
public function removeCCPHID(
DifferentialRevision $revision,
$phid,
$reason_phid) {
self::alterCCPHID($revision, $phid, false, $reason_phid);
}
protected static function alterCCPHID(
DifferentialRevision $revision,
$phid,
$add,
$reason_phid) {
/*
$relationship = new DifferentialRelationship();
$relationship->setRevisionID($revision->getID());
$relationship->setRelation(DifferentialRelationship::RELATION_SUBSCRIBED);
$relationship->setRelatedPHID($phid);
$relationship->setForbidden(!$add);
$relationship->setReasonPHID($reason_phid);
$relationship->replace();
*/
}
public static function addReviewers(
DifferentialRevision $revision,
array $reviewer_ids,
$reason_phid) {
/*
foreach ($reviewer_ids as $reviewer_id) {
$relationship = new DifferentialRelationship();
$relationship->setRevisionID($revision->getID());
$relationship->setRelatedPHID($reviewer_id);
$relationship->setForbidden(false);
$relationship->setReasonPHID($reason_phid);
$relationship->setRelation(DifferentialRelationship::RELATION_REVIEWER);
$relationship->replace();
}
*/
}
public static function removeReviewers(
DifferentialRevision $revision,
array $reviewer_ids,
$reason_phid) {
/*
if (!$reviewer_ids) {
return;
}
foreach ($reviewer_ids as $reviewer_id) {
$relationship = new DifferentialRelationship();
$relationship->setRevisionID($revision->getID());
$relationship->setRelatedPHID($reviewer_id);
$relationship->setForbidden(true);
$relationship->setReasonPHID($reason_phid);
$relationship->setRelation(DifferentialRelationship::RELATION_REVIEWER);
$relationship->replace();
}
*/
}
/*
protected function createFeedback() {
$revision = $this->getRevision();
$feedback = id(new DifferentialFeedback())
->setUserID($this->getActorPHID())
->setRevision($revision)
->setContent($this->getComments())
->setAction('update');
$feedback->save();
return $feedback;
}
*/
}

View file

@ -0,0 +1,17 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus');
phutil_require_module('phabricator', 'applications/differential/mail/ccwelcome');
phutil_require_module('phabricator', 'applications/differential/mail/newdiff');
phutil_require_module('phabricator', 'applications/differential/storage/changeset');
phutil_require_module('phutil', 'utils');
phutil_require_source('DifferentialRevisionEditor.php');

View file

@ -0,0 +1,311 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class DifferentialMail {
const SUBJECT_PREFIX = '[Differential]';
protected $to = array();
protected $cc = array();
protected $actorName;
protected $actorID;
protected $revision;
protected $feedback;
protected $changesets;
protected $inlineComments;
protected $isFirstMailAboutRevision;
protected $isFirstMailToRecipients;
protected $heraldTranscriptURI;
protected $heraldRulesHeader;
public function getActorName() {
return $this->actorName;
}
public function setActorName($actor_name) {
$this->actorName = $actor_name;
return $this;
}
abstract protected function renderSubject();
abstract protected function renderBody();
public function setXHeraldRulesHeader($header) {
$this->heraldRulesHeader = $header;
return $this;
}
public function send() {
$to_phids = $this->getToPHIDs();
if (!$to_phids) {
throw new Exception('No "To:" users provided!');
}
$message_id = $this->getMessageID();
$cc_phids = $this->getCCPHIDs();
$subject = $this->buildSubject();
$body = $this->buildBody();
$mail = new PhabricatorMetaMTAMail();
if ($this->getActorID()) {
$mail->setFrom($this->getActorID());
$mail->setReplyTo($this->getReplyHandlerEmailAddress());
} else {
$mail->setFrom($this->getReplyHandlerEmailAddress());
}
$mail
->addTos($to_phids)
->addCCs($cc_phids)
->setSubject($subject)
->setBody($body)
->setIsHTML($this->shouldMarkMailAsHTML())
->addHeader('Thread-Topic', $this->getRevision()->getTitle())
->addHeader('Thread-Index', $this->generateThreadIndex());
if ($this->isFirstMailAboutRevision()) {
$mail->addHeader('Message-ID', $message_id);
} else {
$mail->addHeader('In-Reply-To', $message_id);
$mail->addHeader('References', $message_id);
}
if ($this->heraldRulesHeader) {
$mail->addHeader('X-Herald-Rules', $this->heraldRulesHeader);
}
$mail->setRelatedPHID($this->getRevision()->getPHID());
// Save this to the MetaMTA queue for later delivery to the MTA.
$mail->save();
}
protected function buildSubject() {
return self::SUBJECT_PREFIX.' '.$this->renderSubject();
}
protected function shouldMarkMailAsHTML() {
return false;
}
protected function buildBody() {
$actions = array();
$body = $this->renderBody();
$body .= <<<EOTEXT
ACTIONS
Reply to comment, or !accept, !reject, !abandon, !resign, or !showdiff.
EOTEXT;
if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) {
$xscript_uri = $this->getHeraldTranscriptURI();
$body .= <<<EOTEXT
MANAGE HERALD RULES
http://todo.com/herald/
WHY DID I GET THIS EMAIL?
{$xscript_uri}
Tip: use the X-Herald-Rules header to filter Herald messages in your client.
EOTEXT;
}
return $body;
}
protected function getReplyHandlerEmailAddress() {
// TODO
$phid = $this->getRevision()->getPHID();
$server = 'todo.example.com';
return "differential+{$phid}@{$server}";
}
protected function formatText($text) {
$text = explode("\n", $text);
foreach ($text as &$line) {
$line = rtrim(' '.$line);
}
unset($line);
return implode("\n", $text);
}
public function setToPHIDs(array $to) {
$this->to = $this->filterContactPHIDs($to);
return $this;
}
public function setCCPHIDs(array $cc) {
$this->cc = $this->filterContactPHIDs($cc);
return $this;
}
protected function filterContactPHIDs(array $phids) {
return $phids;
// TODO: actually do this?
// Differential revisions use Subscriptions for CCs, so any arbitrary
// PHID can end up CC'd to them. Only try to actually send email PHIDs
// which have ToolsHandle types that are marked emailable. If we don't
// filter here, sending the email will fail.
/*
$handles = array();
prep(new ToolsHandleData($phids, $handles));
foreach ($handles as $phid => $handle) {
if (!$handle->isEmailable()) {
unset($handles[$phid]);
}
}
return array_keys($handles);
*/
}
protected function getToPHIDs() {
return $this->to;
}
protected function getCCPHIDs() {
return $this->cc;
}
public function setActorID($actor_id) {
$this->actorID = $actor_id;
return $this;
}
public function getActorID() {
return $this->actorID;
}
public function setRevision($revision) {
$this->revision = $revision;
return $this;
}
public function getRevision() {
return $this->revision;
}
protected function getMessageID() {
$phid = $this->getRevision()->getPHID();
// TODO
return "<differential-rev-{$phid}-req@TODO.com>";
}
public function setFeedback($feedback) {
$this->feedback = $feedback;
return $this;
}
public function getFeedback() {
return $this->feedback;
}
public function setChangesets($changesets) {
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->changesets;
}
public function setInlineComments(array $inline_comments) {
$this->inlineComments = $inline_comments;
return $this;
}
public function getInlineComments() {
return $this->inlineComments;
}
public function renderRevisionDetailLink() {
$uri = $this->getRevisionURI();
return "REVISION DETAIL\n {$uri}";
}
public function getRevisionURI() {
// TODO
return 'http://local.aphront.com/D'.$this->getRevision()->getID();
}
public function setIsFirstMailToRecipients($first) {
$this->isFirstMailToRecipients = $first;
return $this;
}
public function isFirstMailToRecipients() {
return $this->isFirstMailToRecipients;
}
public function setIsFirstMailAboutRevision($first) {
$this->isFirstMailAboutRevision = $first;
return $this;
}
public function isFirstMailAboutRevision() {
return $this->isFirstMailAboutRevision;
}
protected function generateThreadIndex() {
// When threading, Outlook ignores the 'References' and 'In-Reply-To'
// headers that most clients use. Instead, it uses a custom 'Thread-Index'
// header. The format of this header is something like this (from
// camel-exchange-folder.c in Evolution Exchange):
/* A new post to a folder gets a 27-byte-long thread index. (The value
* is apparently unique but meaningless.) Each reply to a post gets a
* 32-byte-long thread index whose first 27 bytes are the same as the
* parent's thread index. Each reply to any of those gets a
* 37-byte-long thread index, etc. The Thread-Index header contains a
* base64 representation of this value.
*/
// The specific implementation uses a 27-byte header for the first email
// a recipient receives, and a random 5-byte suffix (32 bytes total)
// thereafter. This means that all the replies are (incorrectly) siblings,
// but it would be very difficult to keep track of the entire tree and this
// gets us reasonable client behavior.
$base = substr(md5($this->getRevision()->getPHID()), 0, 27);
if (!$this->isFirstMailAboutRevision()) {
// not totally sure, but it seems like outlook orders replies by
// thread-index rather than timestamp, so to get these to show up in the
// right order we use the time as the last 4 bytes.
$base .= ' ' . pack("N", time());
}
return base64_encode($base);
}
public function setHeraldTranscriptURI($herald_transcript_uri) {
$this->heraldTranscriptURI = $herald_transcript_uri;
return $this;
}
public function getHeraldTranscriptURI() {
return $this->heraldTranscriptURI;
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/metamta/storage/mail');
phutil_require_source('DifferentialMail.php');

View file

@ -0,0 +1,39 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialCCWelcomeMail extends DifferentialReviewRequestMail {
protected function renderSubject() {
$revision = $this->getRevision();
return 'Added to CC: '.$revision->getName();
}
protected function renderBody() {
$actor = $this->getActorName();
$name = $this->getRevision()->getName();
$body = array();
$body[] = "{$actor} added you to the CC list for the revision \"{$name}\".";
$body[] = null;
$body[] = $this->renderReviewRequestBody();
return implode("\n", $body);
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/differential/mail/reviewrequest');
phutil_require_source('DifferentialCCWelcomeMail.php');

View file

@ -0,0 +1,35 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialDiffContentMail extends DifferentialMail {
protected $content;
public function __construct(DifferentialRevision $revision, $content) {
$this->setRevision($revision);
$this->content = $content;
}
protected function renderSubject() {
return "Content: ".$this->getRevision()->getName();
}
protected function renderBody() {
return $this->content;
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/differential/mail/base');
phutil_require_source('DifferentialDiffContentMail.php');

View file

@ -0,0 +1,114 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialFeedbackMail extends DifferentialMail {
protected $changedByCommit;
public function setChangedByCommit($changed_by_commit) {
$this->changedByCommit = $changed_by_commit;
return $this;
}
public function getChangedByCommit() {
return $this->changedByCommit;
}
public function __construct(
DifferentialRevision $revision,
$actor_id,
DifferentialFeedback $feedback,
array $changesets,
array $inline_comments) {
$this->setRevision($revision);
$this->setActorID($actor_id);
$this->setFeedback($feedback);
$this->setChangesets($changesets);
$this->setInlineComments($inline_comments);
}
protected function renderSubject() {
$revision = $this->getRevision();
$verb = $this->getVerb();
return ucwords($verb).': '.$revision->getName();
}
protected function getVerb() {
$feedback = $this->getFeedback();
$action = $feedback->getAction();
$verb = DifferentialAction::getActionVerb($action);
return $verb;
}
protected function renderBody() {
$feedback = $this->getFeedback();
$actor = $this->getActorName();
$name = $this->getRevision()->getName();
$verb = $this->getVerb();
$body = array();
$body[] = "{$actor} has {$verb} the revision \"{$name}\".";
$body[] = null;
$content = $feedback->getContent();
if (strlen($content)) {
$body[] = $this->formatText($content);
$body[] = null;
}
if ($this->getChangedByCommit()) {
$body[] = 'CHANGED PRIOR TO COMMIT';
$body[] = ' This revision was updated prior to commit.';
$body[] = null;
}
$inlines = $this->getInlineComments();
if ($inlines) {
$body[] = 'INLINE COMMENTS';
$changesets = $this->getChangesets();
foreach ($inlines as $inline) {
$changeset = $changesets[$inline->getChangesetID()];
if (!$changeset) {
throw new Exception('Changeset missing!');
}
$file = $changeset->getFilename();
$line = $inline->renderLineRange();
$content = $inline->getContent();
$body[] = $this->formatText("{$file}:{$line} {$content}");
}
$body[] = null;
}
$body[] = $this->renderRevisionDetailLink();
$revision = $this->getRevision();
if ($revision->getStatus() == DifferentialRevisionStatus::COMMITTED) {
$rev_ref = $revision->getRevisionRef();
if ($rev_ref) {
$body[] = " Detail URL: ".$rev_ref->getDetailURL();
}
}
$body[] = null;
return implode("\n", $body);
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/differential/constants/action');
phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus');
phutil_require_module('phabricator', 'applications/differential/mail/base');
phutil_require_source('DifferentialFeedbackMail.php');

View file

@ -0,0 +1,65 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialNewDiffMail extends DifferentialReviewRequestMail {
protected function renderSubject() {
$revision = $this->getRevision();
$line_count = $revision->getLineCount();
$lines = ($line_count == 1 ? "1 line" : "{$line_count} lines");
if ($this->isFirstMailToRecipients()) {
$verb = 'Request';
} else {
$verb = 'Updated';
}
return "{$verb} ({$lines}): ".$revision->getTitle();
}
protected function buildSubject() {
if (!$this->isFirstMailToRecipients()) {
return parent::buildSubject();
}
$prefix = self::SUBJECT_PREFIX;
$subject = $this->renderSubject();
return "{$prefix} {$subject}";
}
protected function renderBody() {
$actor = $this->getActorName();
$name = $this->getRevision()->getTitle();
$body = array();
if ($this->isFirstMailToRecipients()) {
$body[] = "{$actor} requested code review of \"{$name}\".";
} else {
$body[] = "{$actor} updated the revision \"{$name}\".";
}
$body[] = null;
$body[] = $this->renderReviewRequestBody();
return implode("\n", $body);
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/differential/mail/reviewrequest');
phutil_require_source('DifferentialNewDiffMail.php');

View file

@ -0,0 +1,74 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class DifferentialReviewRequestMail extends DifferentialMail {
protected $comments;
public function setComments($comments) {
$this->comments = $comments;
return $this;
}
public function getComments() {
return $this->comments;
}
public function __construct(
DifferentialRevision $revision,
$actor_id,
array $changesets) {
$this->setRevision($revision);
$this->setActorID($actor_id);
$this->setChangesets($changesets);
}
protected function renderReviewRequestBody() {
$revision = $this->getRevision();
$body = array();
if ($this->isFirstMailToRecipients()) {
$body[] = $this->formatText($revision->getSummary());
$body[] = null;
$body[] = 'TEST PLAN';
$body[] = $this->formatText($revision->getTestPlan());
$body[] = null;
} else {
if (strlen($this->getComments())) {
$body[] = $this->formatText($this->getComments());
$body[] = null;
}
}
$body[] = $this->renderRevisionDetailLink();
$body[] = null;
$changesets = $this->getChangesets();
if ($changesets) {
$body[] = 'AFFECTED FILES';
foreach ($changesets as $changeset) {
$body[] = ' '.$changeset->getFilename();
}
$body[] = null;
}
return implode("\n", $body);
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/differential/mail/base');
phutil_require_source('DifferentialReviewRequestMail.php');

View file

@ -18,7 +18,7 @@
class DifferentialRevision extends DifferentialDAO {
protected $name;
protected $title;
protected $status;
protected $summary;
@ -33,4 +33,26 @@ class DifferentialRevision extends DifferentialDAO {
protected $lineCount;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID('DREV');
}
public function loadRelationships() {
}
public function getReviewers() {
return array();
}
public function getCCPHIDs() {
return array();
}
}

View file

@ -39,6 +39,11 @@ final class AphrontFormView extends AphrontView {
return $this;
}
public function addHiddenInput($key, $value) {
$this->data[$key] = $value;
return $this;
}
public function render() {
require_celerity_resource('aphront-form-view-css');
return phutil_render_tag(
@ -59,6 +64,9 @@ final class AphrontFormView extends AphrontView {
);
$inputs = array();
foreach ($data as $key => $value) {
if ($value === null) {
continue;
}
$inputs[] = phutil_render_tag(
'input',
array(

View file

@ -18,16 +18,36 @@
class AphrontFormTextAreaControl extends AphrontFormControl {
const HEIGHT_VERY_SHORT = 'very-short';
const HEIGHT_SHORT = 'short';
private $height;
public function setHeight($height) {
$this->height = $height;
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-textarea';
}
protected function renderInput() {
$height_class = null;
switch ($this->height) {
case self::HEIGHT_VERY_SHORT:
case self::HEIGHT_SHORT:
$height_class = 'aphront-textarea-'.$this->height;
break;
}
return phutil_render_tag(
'textarea',
array(
'name' => $this->getName(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'class' => $height_class,
),
phutil_escape_html($this->getValue()));
}

View file

@ -114,10 +114,11 @@ class PhabricatorStandardPageView extends AphrontPageView {
$login_stuff = null;
$request = $this->getRequest();
$user = $request->getUser();
if ($user->getPHID()) {
$login_stuff = 'Logged in as '.phutil_escape_html($user->getUsername());
if ($request) {
$user = $request->getUser();
if ($user->getPHID()) {
$login_stuff = 'Logged in as '.phutil_escape_html($user->getUsername());
}
}
return

View file

@ -53,6 +53,10 @@
margin: 0.5em 0 0em 2%;
}
.aphront-form-control-textarea textarea.aphront-textarea-very-short {
height: 3em;
}
.aphront-form-control-select .aphront-form-input {
padding-top: 2px;
}