Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/differential/editor/revision/DifferentialRevisionEditor.php b/src/applications/differential/editor/revision/DifferentialRevisionEditor.php
index 7842884e2c..a8a9769d00 100644
--- a/src/applications/differential/editor/revision/DifferentialRevisionEditor.php
+++ b/src/applications/differential/editor/revision/DifferentialRevisionEditor.php
@@ -1,893 +1,903 @@
<?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;
private $auxiliaryFields = array();
private $contentSource;
public function __construct(DifferentialRevision $revision, $actor_phid) {
$this->revision = $revision;
$this->actorPHID = $actor_phid;
}
public static function newRevisionFromConduitWithDiff(
array $fields,
DifferentialDiff $diff,
$user_phid) {
$revision = new DifferentialRevision();
$revision->setPHID($revision->generatePHID());
$revision->setAuthorPHID($user_phid);
$revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
$editor = new DifferentialRevisionEditor($revision, $user_phid);
$editor->copyFieldsFromConduit($fields);
$editor->addDiff($diff, null);
$editor->save();
return $revision;
}
public function copyFieldsFromConduit(array $fields) {
$revision = $this->revision;
$revision->loadRelationships();
$aux_fields = DifferentialFieldSelector::newSelector()
->getFieldSpecifications();
foreach ($aux_fields as $key => $aux_field) {
$aux_field->setRevision($revision);
if (!$aux_field->shouldAppearOnCommitMessage()) {
unset($aux_fields[$key]);
}
}
$aux_fields = mpull($aux_fields, null, 'getCommitMessageKey');
foreach ($fields as $field => $value) {
if (empty($aux_fields[$field])) {
throw new Exception(
"Parsed commit message contains unrecognized field '{$field}'.");
}
$aux_fields[$field]->setValueFromParsedCommitMessage($value);
}
$aux_fields = array_values($aux_fields);
$this->setAuxiliaryFields($aux_fields);
}
public function setAuxiliaryFields(array $auxiliary_fields) {
$this->auxiliaryFields = $auxiliary_fields;
return $this;
}
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 setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
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->getAuthorPHID() === null) {
$revision->setAuthorPHID($this->getActorPHID());
}
if ($revision->getSummary() === null) {
$revision->setSummary('');
}
if ($revision->getTestPlan() === null) {
$revision->setTestPlan('');
}
$revision->save();
}
$revision->loadRelationships();
$this->willWriteRevision();
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());
$adapter = new HeraldDifferentialRevisionAdapter(
$revision,
$diff);
$adapter->setExplicitCCs($new['ccs']);
$adapter->setExplicitReviewers($new['rev']);
$adapter->setForbiddenCCs($revision->getUnsubscribedPHIDs());
$xscript = HeraldEngine::loadAndApplyRules($adapter);
$xscript_uri = PhabricatorEnv::getProductionURI(
'/herald/transcript/'.$xscript->getID().'/');
$xscript_phid = $xscript->getPHID();
$xscript_header = $xscript->getXHeraldRulesHeader();
$xscript_header = HeraldTranscript::saveXHeraldRulesHeader(
$revision->getPHID(),
$xscript_header);
$sub = array(
'rev' => array(),
'ccs' => $adapter->getCCsAddedByHerald(),
);
$rem_ccs = $adapter->getCCsRemovedByHerald();
} 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::alterReviewers(
$revision,
$this->reviewers,
array_keys($rem['rev']),
array_keys($add['rev']),
$this->actorPHID);
/*
// TODO: When Herald is brought over, run through this stuff to figure
// out which adds are Herald's fault.
// TODO: Still need to do this.
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();
}
}
foreach (array_keys($rem['ccs']) as $id) {
if (empty($new['ccs'][$id])) {
$reason_phid = $this->getActorPHID();
} else {
$reason_phid = 'TODO';//$xscript_phid;
}
}
}
*/
self::alterCCs(
$revision,
$this->cc,
array_keys($rem['ccs']),
array_keys($add['ccs']),
$this->actorPHID);
$this->updateAuxiliaryFields();
// Add the author and users included from Herald rules 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;
if ($diff) {
$add['rev'] += $adapter->getEmailPHIDsAddedByHerald();
}
} else {
$stable['rev'][$this->getActorPHID()] = true;
if ($diff) {
$stable['rev'] += $adapter->getEmailPHIDsAddedByHerald();
}
}
}
$mail = array();
$phids = array($this->getActorPHID());
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$actor_handle = $handles[$this->getActorPHID()];
$changesets = null;
$comment = null;
if ($diff) {
$changesets = $diff->loadChangesets();
// TODO: This should probably be in DifferentialFeedbackEditor?
if (!$is_new) {
$comment = $this->createComment();
}
if ($comment) {
$mail[] = id(new DifferentialNewDiffMail(
$revision,
$actor_handle,
$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.
$diff->setDescription(substr($this->getComments(), 0, 80));
$diff->save();
$this->updateAffectedPathTable($revision, $diff, $changesets);
$this->updateRevisionHashTable($revision, $diff);
// 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->loadActiveDiff();
if ($diff) {
$changesets = $diff->loadChangesets();
} else {
$changesets = array();
}
}
$revision->save();
$this->didWriteRevision();
$event_data = array(
'revision_id' => $revision->getID(),
'revision_phid' => $revision->getPHID(),
'revision_name' => $revision->getTitle(),
'revision_author_phid' => $revision->getAuthorPHID(),
'action' => $is_new
? DifferentialAction::ACTION_CREATE
: DifferentialAction::ACTION_UPDATE,
'feedback_content' => $is_new
? phutil_utf8_shorten($revision->getSummary(), 140)
: $this->getComments(),
'actor_phid' => $revision->getAuthorPHID(),
);
id(new PhabricatorTimelineEvent('difx', $event_data))
->recordEvent();
id(new PhabricatorFeedStoryPublisher())
->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL)
->setStoryData($event_data)
->setStoryTime(time())
->setStoryAuthorPHID($revision->getAuthorPHID())
->setRelatedPHIDs(
array(
$revision->getPHID(),
$revision->getAuthorPHID(),
))
->publish();
// TODO
// $revision->saveTransaction();
// TODO: Move this into a worker task thing.
PhabricatorSearchDifferentialIndexer::indexRevision($revision);
if ($this->silentUpdate) {
return;
}
$revision->loadRelationships();
if ($add['rev']) {
$message = id(new DifferentialNewDiffMail(
$revision,
$actor_handle,
$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,
$actor_handle,
$changesets))
->setIsFirstMailToRecipients(true)
->setToPHIDs(array_keys($add['ccs']));
}
foreach ($mail as $message) {
$message->setHeraldTranscriptURI($xscript_uri);
$message->setXHeraldRulesHeader($xscript_header);
$message->send();
}
}
public static function addCCAndUpdateRevision(
$revision,
$phid,
$reason) {
self::addCC($revision, $phid, $reason);
$unsubscribed = $revision->getUnsubscribed();
if (isset($unsubscribed[$phid])) {
unset($unsubscribed[$phid]);
$revision->setUnsubscribed($unsubscribed);
$revision->save();
}
}
public static function removeCCAndUpdateRevision(
$revision,
$phid,
$reason) {
self::removeCC($revision, $phid, $reason);
$unsubscribed = $revision->getUnsubscribed();
if (empty($unsubscribed[$phid])) {
$unsubscribed[$phid] = true;
$revision->setUnsubscribed($unsubscribed);
$revision->save();
}
}
public static function addCC(
DifferentialRevision $revision,
$phid,
$reason) {
return self::alterCCs(
$revision,
$revision->getCCPHIDs(),
$rem = array(),
$add = array($phid),
$reason);
}
public static function removeCC(
DifferentialRevision $revision,
$phid,
$reason) {
return self::alterCCs(
$revision,
$revision->getCCPHIDs(),
$rem = array($phid),
$add = array(),
$reason);
}
protected static function alterCCs(
DifferentialRevision $revision,
array $stable_phids,
array $rem_phids,
array $add_phids,
$reason_phid) {
return self::alterRelationships(
$revision,
$stable_phids,
$rem_phids,
$add_phids,
$reason_phid,
DifferentialRevision::RELATION_SUBSCRIBED);
}
public static function alterReviewers(
DifferentialRevision $revision,
array $stable_phids,
array $rem_phids,
array $add_phids,
$reason_phid) {
return self::alterRelationships(
$revision,
$stable_phids,
$rem_phids,
$add_phids,
$reason_phid,
DifferentialRevision::RELATION_REVIEWER);
}
private static function alterRelationships(
DifferentialRevision $revision,
array $stable_phids,
array $rem_phids,
array $add_phids,
$reason_phid,
$relation_type) {
$rem_map = array_fill_keys($rem_phids, true);
$add_map = array_fill_keys($add_phids, true);
$seq_map = array_values($stable_phids);
$seq_map = array_flip($seq_map);
foreach ($rem_map as $phid => $ignored) {
if (!isset($seq_map[$phid])) {
$seq_map[$phid] = count($seq_map);
}
}
foreach ($add_map as $phid => $ignored) {
if (!isset($seq_map[$phid])) {
$seq_map[$phid] = count($seq_map);
}
}
$raw = $revision->getRawRelations($relation_type);
$raw = ipull($raw, null, 'objectPHID');
$sequence = count($seq_map);
foreach ($raw as $phid => $ignored) {
if (isset($seq_map[$phid])) {
$raw[$phid]['sequence'] = $seq_map[$phid];
} else {
$raw[$phid]['sequence'] = $sequence++;
}
}
$raw = isort($raw, 'sequence');
foreach ($raw as $phid => $ignored) {
if (isset($rem_map[$phid])) {
unset($raw[$phid]);
}
}
foreach ($add_phids as $add) {
$raw[$add] = array(
'objectPHID' => $add,
'sequence' => idx($seq_map, $add, $sequence++),
'reasonPHID' => $reason_phid,
);
}
$conn_w = $revision->establishConnection('w');
$sql = array();
foreach ($raw as $relation) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s, %s, %d, %s)',
$revision->getID(),
$relation_type,
$relation['objectPHID'],
$relation['sequence'],
$relation['reasonPHID']);
}
$conn_w->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d AND relation = %s',
DifferentialRevision::RELATIONSHIP_TABLE,
$revision->getID(),
$relation_type);
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T
(revisionID, relation, objectPHID, sequence, reasonPHID)
VALUES %Q',
DifferentialRevision::RELATIONSHIP_TABLE,
implode(', ', $sql));
}
$conn_w->saveTransaction();
$revision->loadRelationships();
}
private function createComment() {
$revision_id = $this->revision->getID();
$comment = id(new DifferentialComment())
->setAuthorPHID($this->getActorPHID())
->setRevisionID($revision_id)
->setContent($this->getComments())
->setAction('update');
if ($this->contentSource) {
$comment->setContentSource($this->contentSource);
}
$comment->save();
return $comment;
}
private function updateAuxiliaryFields() {
$aux_map = array();
foreach ($this->auxiliaryFields as $aux_field) {
$key = $aux_field->getStorageKey();
if ($key !== null) {
$val = $aux_field->getValueForStorage();
$aux_map[$key] = $val;
}
}
if (!$aux_map) {
return;
}
$revision = $this->revision;
$fields = id(new DifferentialAuxiliaryField())->loadAllWhere(
'revisionPHID = %s AND name IN (%Ls)',
$revision->getPHID(),
array_keys($aux_map));
$fields = mpull($fields, null, 'getName');
foreach ($aux_map as $key => $val) {
$obj = idx($fields, $key);
if (!strlen($val)) {
// If the new value is empty, just delete the old row if one exists and
// don't add a new row if it doesn't.
if ($obj) {
$obj->delete();
}
} else {
if (!$obj) {
$obj = new DifferentialAuxiliaryField();
$obj->setRevisionPHID($revision->getPHID());
$obj->setName($key);
}
if ($obj->getValue() !== $val) {
$obj->setValue($val);
$obj->save();
}
}
}
}
private function willWriteRevision() {
foreach ($this->auxiliaryFields as $aux_field) {
$aux_field->willWriteRevision($this);
}
}
private function didWriteRevision() {
foreach ($this->auxiliaryFields as $aux_field) {
$aux_field->didWriteRevision($this);
}
}
/**
* 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,
array $changesets) {
$project = $diff->loadArcanistProject();
if (!$project) {
// Probably an old revision from before projects.
return;
}
$repository = $project->loadRepository();
if (!$repository) {
// Probably no project <-> repository link, or the repository where the
// project 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));
}
}
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $path_prefix.'/'.$changeset->getFileName();
}
- $path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs($paths);
+ // 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_map = id(new DiffusionPathIDQuery($all_paths))->loadPathIDs();
$table = new DifferentialAffectedPath();
$conn_w = $table->establishConnection('w');
$sql = array();
- foreach ($paths as $path) {
+ foreach ($all_paths as $path) {
$path_id = idx($path_map, $path);
if (!$path_id) {
// Don't bother creating these, it probably means we're either adding
// a file (in which case having this row is irrelevant since Diffusion
// won't be querying for it) or something is misconfigured (in which
// case we'd just be writing garbage).
continue;
}
$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(
DifferentialRevisionHash::HASH_GIT_COMMIT,
$commit['commit'],
);
$hashes[] = array(
DifferentialRevisionHash::HASH_GIT_TREE,
$commit['tree'],
);
}
break;
case DifferentialRevisionControlSystem::MERCURIAL:
foreach ($data as $commit) {
$hashes[] = array(
DifferentialRevisionHash::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',
DifferentialRevisionHash::TABLE_NAME,
$revision->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (revisionID, type, hash) VALUES %Q',
DifferentialRevisionHash::TABLE_NAME,
implode(', ', $sql));
}
}
}
diff --git a/src/applications/diffusion/controller/base/DiffusionController.php b/src/applications/diffusion/controller/base/DiffusionController.php
index 132a9854be..0d5fa73c0c 100644
--- a/src/applications/diffusion/controller/base/DiffusionController.php
+++ b/src/applications/diffusion/controller/base/DiffusionController.php
@@ -1,248 +1,286 @@
<?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 DiffusionController extends PhabricatorController {
protected $diffusionRequest;
public function willProcessRequest(array $data) {
$this->diffusionRequest = DiffusionRequest::newFromAphrontRequestDictionary(
$data);
}
public function setDiffusionRequest(DiffusionRequest $request) {
$this->diffusionRequest = $request;
return $this;
}
protected function getDiffusionRequest() {
return $this->diffusionRequest;
}
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Diffusion');
$page->setBaseURI('/diffusion/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x89\x88");
$page->setTabs(
array(
'help' => array(
'href' => PhabricatorEnv::getDoclink(
'article/Diffusion_User_Guide.html'),
'name' => 'Help',
),
),
null);
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
final protected function buildSideNav($selected, $has_change_view) {
$nav = new AphrontSideNavView();
$navs = array(
'history' => 'History View',
'browse' => 'Browse View',
'change' => 'Change View',
);
if (!$has_change_view) {
unset($navs['change']);
}
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$callsign = $repository->getCallsign();
$branch_uri = $drequest->getBranchURIComponent($drequest->getBranch());
$path_uri = $branch_uri.$drequest->getPath();
$commit_uri = null;
$raw_commit = $drequest->getRawCommit();
if ($raw_commit) {
$commit_uri = ';'.$drequest->getCommitURIComponent($raw_commit);
}
foreach ($navs as $uri => $name) {
$nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/{$uri}/{$path_uri}{$commit_uri}",
'class' =>
($uri == $selected
? 'aphront-side-nav-selected'
: null),
),
$name));
}
return $nav;
}
public function buildCrumbs(array $spec = array()) {
$crumbs = new AphrontCrumbsView();
$crumb_list = $this->buildCrumbList($spec);
$crumbs->setCrumbs($crumb_list);
return $crumbs;
}
+ protected function buildOpenRevisions() {
+ $drequest = $this->getDiffusionRequest();
+ $repository = $drequest->getRepository();
+ $path = $drequest->getPath();
+
+ $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();
+ $path_id = idx($path_map, $path);
+ if (!$path_id) {
+ return null;
+ }
+
+ $revisions = id(new DifferentialRevisionQuery())
+ ->withPath($repository->getID(), $path_id)
+ ->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
+ ->setOrder(DifferentialRevisionQuery::ORDER_PATH_MODIFIED)
+ ->setLimit(10)
+ ->needRelationships(true)
+ ->execute();
+
+ if (!$revisions) {
+ return null;
+ }
+
+ $view = id(new DifferentialRevisionListView())
+ ->setRevisions($revisions)
+ ->setUser($this->getRequest()->getUser());
+
+ $phids = $view->getRequiredHandlePHIDs();
+ $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
+ $view->setHandles($handles);
+
+ $panel = new AphrontPanelView();
+ $panel->setHeader('Pending Differential Revisions');
+ $panel->appendChild($view);
+
+ return $panel;
+ }
+
private function buildCrumbList(array $spec = array()) {
$drequest = $this->getDiffusionRequest();
$crumb_list = array();
$repository = $drequest->getRepository();
if ($repository) {
$crumb_list[] = phutil_render_tag(
'a',
array(
'href' => '/diffusion/',
),
'Diffusion');
} else {
$crumb_list[] = 'Diffusion';
return $crumb_list;
}
$callsign = $repository->getCallsign();
$repository_name = phutil_escape_html($repository->getName()).' Repository';
$branch_name = $drequest->getBranch();
if ($branch_name) {
$repository_name .= ' ('.phutil_escape_html($branch_name).')';
}
$branch_uri = $drequest->getBranchURIComponent($drequest->getBranch());
if (empty($spec['view']) && empty($spec['commit'])) {
$crumb_list[] = $repository_name;
return $crumb_list;
}
$crumb_list[] = phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/",
),
$repository_name);
$raw_commit = $drequest->getRawCommit();
if (isset($spec['commit'])) {
$crumb_list[] = "r{$callsign}{$raw_commit}";
return $crumb_list;
}
$view = $spec['view'];
$path = null;
if (isset($spec['path'])) {
$path = $drequest->getPath();
}
if ($raw_commit) {
$commit_link = DiffusionView::linkCommit(
$repository,
$raw_commit);
} else {
$commit_link = '';
}
switch ($view) {
case 'history':
$view_name = 'History';
break;
case 'browse':
$view_name = 'Browse';
break;
case 'change':
$view_name = 'Change';
$crumb_list[] = phutil_escape_html($path).' ('.$commit_link.')';
return $crumb_list;
}
$view_root_uri = "/diffusion/{$callsign}/{$view}/{$branch_uri}";
$jump_href = $view_root_uri;
$view_tail_uri = null;
if ($raw_commit) {
$view_tail_uri = ';'.$drequest->getCommitURIComponent($raw_commit);
}
if (!strlen($path)) {
$crumb_list[] = $view_name;
} else {
$crumb_list[] = phutil_render_tag(
'a',
array(
'href' => $view_root_uri.$view_tail_uri,
),
$view_name);
$path_parts = explode('/', $path);
do {
$last = array_pop($path_parts);
} while ($last == '');
$path_sections = array();
$thus_far = '';
foreach ($path_parts as $path_part) {
$thus_far .= $path_part.'/';
$path_sections[] = phutil_render_tag(
'a',
array(
'href' => $view_root_uri.$thus_far.$view_tail_uri,
),
phutil_escape_html($path_part));
}
$path_sections[] = phutil_escape_html($last);
$path_sections = '/'.implode('/', $path_sections);
$jump_href = $view_root_uri.$thus_far.$last;
$crumb_list[] = $path_sections;
}
$last_crumb = array_pop($crumb_list);
if ($raw_commit) {
$jump_link = phutil_render_tag(
'a',
array(
'href' => $jump_href,
),
'Jump to HEAD');
$last_crumb .= " @ {$commit_link} ({$jump_link})";
} else {
$last_crumb .= " @ HEAD";
}
$crumb_list[] = $last_crumb;
return $crumb_list;
}
}
diff --git a/src/applications/diffusion/controller/base/__init__.php b/src/applications/diffusion/controller/base/__init__.php
index d154ac6eba..2be5bb582b 100644
--- a/src/applications/diffusion/controller/base/__init__.php
+++ b/src/applications/diffusion/controller/base/__init__.php
@@ -1,21 +1,26 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/response/webpage');
phutil_require_module('phabricator', 'applications/base/controller/base');
+phutil_require_module('phabricator', 'applications/differential/query/revision');
+phutil_require_module('phabricator', 'applications/differential/view/revisionlist');
+phutil_require_module('phabricator', 'applications/diffusion/query/pathid/base');
phutil_require_module('phabricator', 'applications/diffusion/request/base');
phutil_require_module('phabricator', 'applications/diffusion/view/base');
+phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/layout/crumbs');
+phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phabricator', 'view/layout/sidenav');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');
phutil_require_source('DiffusionController.php');
diff --git a/src/applications/diffusion/controller/browse/DiffusionBrowseController.php b/src/applications/diffusion/controller/browse/DiffusionBrowseController.php
index f67b9cfaf2..859f6f71b8 100644
--- a/src/applications/diffusion/controller/browse/DiffusionBrowseController.php
+++ b/src/applications/diffusion/controller/browse/DiffusionBrowseController.php
@@ -1,135 +1,137 @@
<?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 DiffusionBrowseController extends DiffusionController {
public function processRequest() {
$drequest = $this->diffusionRequest;
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
$results = $browse_query->loadPaths();
$content = array();
$content[] = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
if (!$results) {
// TODO: Format all these commits into nice VCS-agnostic links, and
// below.
$commit = $drequest->getCommit();
$callsign = $drequest->getRepository()->getCallsign();
if ($commit) {
$commit = "r{$callsign}{$commit}";
} else {
$commit = 'HEAD';
}
switch ($browse_query->getReasonForEmptyResultSet()) {
case DiffusionBrowseQuery::REASON_IS_NONEXISTENT:
$title = 'Path Does Not Exist';
// TODO: Under git, this error message should be more specific. It
// may exist on some other branch.
$body = "This path does not exist anywhere.";
$severity = AphrontErrorView::SEVERITY_ERROR;
break;
case DiffusionBrowseQuery::REASON_IS_EMPTY:
$title = 'Empty Directory';
$body = "This path was an empty directory at {$commit}.\n";
$severity = AphrontErrorView::SEVERITY_NOTICE;
break;
case DiffusionBrowseQuery::REASON_IS_DELETED:
$deleted = $browse_query->getDeletedAtCommit();
$existed = $browse_query->getExistedAtCommit();
$deleted = "r{$callsign}{$deleted}";
$existed = "r{$callsign}{$existed}";
$title = 'Path Was Deleted';
$body = "This path does not exist at {$commit}. It was deleted in ".
"{$deleted} and last existed at {$existed}.";
$severity = AphrontErrorView::SEVERITY_WARNING;
break;
case DiffusionBrowseQuery::REASON_IS_FILE:
$controller = new DiffusionBrowseFileController($this->getRequest());
$controller->setDiffusionRequest($drequest);
return $this->delegateToController($controller);
break;
case DiffusionBrowseQuery::REASON_IS_UNTRACKED_PARENT:
$subdir = $drequest->getRepository()->getDetail('svn-subpath');
$title = 'Directory Not Tracked';
$body =
"This repository is configured to track only one subdirectory ".
"of the entire repository ('".phutil_escape_html($subdir)."'), ".
"but you aren't looking at something in that subdirectory, so no ".
"information is available.";
$severity = AphrontErrorView::SEVERITY_WARNING;
break;
default:
throw new Exception("Unknown failure reason!");
}
$error_view = new AphrontErrorView();
$error_view->setSeverity($severity);
$error_view->setTitle($title);
$error_view->appendChild('<p>'.$body.'</p>');
$content[] = $error_view;
} else {
$phids = array();
foreach ($results as $result) {
$data = $result->getLastCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
}
}
$phids = array_keys($phids);
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$browse_table = new DiffusionBrowseTableView();
$browse_table->setDiffusionRequest($drequest);
$browse_table->setHandles($handles);
$browse_table->setPaths($results);
$browse_panel = new AphrontPanelView();
$browse_panel->appendChild($browse_table);
$content[] = $browse_panel;
}
+ $content[] = $this->buildOpenRevisions();
+
$nav = $this->buildSideNav('browse', false);
$nav->appendChild($content);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => basename($drequest->getPath()),
));
}
}
diff --git a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php
index 3dacdd7fce..a0ac7b1a1d 100644
--- a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php
+++ b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php
@@ -1,429 +1,431 @@
<?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 DiffusionBrowseFileController extends DiffusionController {
// Image types we want to display inline using <img> tags
protected $imageTypes = array(
'png' => 'image/png',
'gif' => 'image/gif',
'ico' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg'=> 'image/jpeg'
);
// Document types that should trigger link to ?view=raw
protected $documentTypes = array(
'pdf'=> 'application/pdf',
'ps' => 'application/postscript',
);
public function processRequest() {
// Build the view selection form.
$select_map = array(
'highlighted' => 'View as Highlighted Text',
'blame' => 'View as Highlighted Text with Blame',
'plain' => 'View as Plain Text',
'plainblame' => 'View as Plain Text with Blame',
'raw' => 'View as raw document',
);
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
$path = $drequest->getPath();
$selected = $request->getStr('view');
$needs_blame = ($selected == 'blame' || $selected == 'plainblame');
$file_query = DiffusionFileContentQuery::newFromDiffusionRequest(
$this->diffusionRequest);
$file_query->setNeedsBlame($needs_blame);
$file_query->loadFileContent();
$data = $file_query->getRawData();
if ($selected === 'raw') {
$response = new AphrontFileResponse();
$response->setContent($data);
$mime_type = $this->getDocumentType($path);
if ($mime_type) {
$response->setMimeType($mime_type);
} else {
$as_filename = idx(pathinfo($path), 'basename');
$response->setDownload($as_filename);
}
return $response;
}
$select = '<select name="view">';
foreach ($select_map as $k => $v) {
$option = phutil_render_tag(
'option',
array(
'value' => $k,
'selected' => ($k == $selected) ? 'selected' : null,
),
phutil_escape_html($v));
$select .= $option;
}
$select .= '</select>';
require_celerity_resource('diffusion-source-css');
$view_select_panel = new AphrontPanelView();
$view_select_form = phutil_render_tag(
'form',
array(
'action' => $request->getRequestURI(),
'method' => 'get',
'class' => 'diffusion-browse-type-form',
),
$select.
'<button>View</button>');
$view_select_panel->appendChild($view_select_form);
$view_select_panel->appendChild('<div style="clear: both;"></div>');
// Build the content of the file.
$corpus = $this->buildCorpus(
$selected,
$file_query,
$needs_blame,
$drequest,
$path,
$data
);
// Render the page.
$content = array();
$content[] = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
$content[] = $view_select_panel;
$content[] = $corpus;
+ $content[] = $this->buildOpenRevisions();
$nav = $this->buildSideNav('browse', true);
$nav->appendChild($content);
+
$basename = basename($this->getDiffusionRequest()->getPath());
return $this->buildStandardPageResponse(
$nav,
array(
'title' => $basename,
));
}
/**
* Returns a content-type corrsponding to an image file extension
*
* @param string $path File path
* @return mixed A content-type string or NULL if path doesn't end with a
* recognized image extension
*/
public function getImageType($path) {
$ext = pathinfo($path);
$ext = idx($ext, 'extension');
return idx($this->imageTypes, $ext);
}
/**
* Returns a content-type corresponding to an document file extension
*
* @param string $path File path
* @return mixed A content-type string or NULL if path doesn't end with a
* recognized document extension
*/
public function getDocumentType($path) {
$ext = pathinfo($path);
$ext = idx($ext, 'extension');
return idx($this->documentTypes, $ext);
}
private function buildCorpus($selected,
$file_query,
$needs_blame,
$drequest,
$path,
$data) {
$image_type = $this->getImageType($path);
if ($image_type && !$selected) {
$corpus = phutil_render_tag(
'img',
array(
'style' => 'padding-bottom: 10px',
'src' => 'data:'.$image_type.';base64,'.base64_encode($data),
)
);
return $corpus;
}
$document_type = $this->getDocumentType($path);
if (($document_type && !$selected) || !phutil_is_utf8($data)) {
$data = $file_query->getRawData();
$document_type_description = $document_type ? $document_type : 'binary';
$corpus = phutil_render_tag(
'p',
array(
'style' => 'text-align: center;'
),
phutil_render_tag(
'a',
array(
'href' => '?view=raw',
'class' => 'button'
),
"View $document_type_description"
)
);
return $corpus;
}
// TODO: blame of blame.
switch ($selected) {
case 'plain':
$style =
"margin: 1em 2em; width: 90%; height: 80em; font-family: monospace";
$corpus = phutil_render_tag(
'textarea',
array(
'style' => $style,
),
phutil_escape_html($file_query->getRawData()));
break;
case 'plainblame':
$style =
"margin: 1em 2em; width: 90%; height: 80em; font-family: monospace";
list($text_list, $rev_list, $blame_dict) =
$file_query->getBlameData();
$rows = array();
foreach ($text_list as $k => $line) {
$rev = $rev_list[$k];
$author = $blame_dict[$rev]['author'];
$rows[] =
sprintf("%-10s %-20s %s", substr($rev, 0, 7), $author, $line);
}
$corpus = phutil_render_tag(
'textarea',
array(
'style' => $style,
),
phutil_escape_html(implode("\n", $rows)));
break;
case 'highlighted':
case 'blame':
default:
require_celerity_resource('syntax-highlighting-css');
list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData();
$text_list = implode("\n", $text_list);
$text_list = PhabricatorSyntaxHighlighter::highlightWithFilename(
$path,
$text_list);
$text_list = explode("\n", $text_list);
$rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict,
$needs_blame, $drequest, $file_query, $selected);
$corpus_table = phutil_render_tag(
'table',
array(
'class' => "diffusion-source remarkup-code PhabricatorMonospaced",
),
implode("\n", $rows));
$corpus = phutil_render_tag(
'div',
array(
'style' => 'padding: 0pt 2em;',
),
$corpus_table);
break;
}
return $corpus;
}
private function buildDisplayRows($text_list, $rev_list, $blame_dict,
$needs_blame, DiffusionRequest $drequest, $file_query, $selected) {
$last_rev = null;
$color = null;
$rows = array();
$n = 1;
$view = $this->getRequest()->getStr('view');
if ($blame_dict) {
$epoch_list = ipull($blame_dict, 'epoch');
$max = max($epoch_list);
$min = min($epoch_list);
$range = $max - $min + 1;
} else {
$range = 1;
}
foreach ($text_list as $k => $line) {
if ($needs_blame) {
// If the line's rev is same as the line above, show empty content
// with same color; otherwise generate blame info. The newer a change
// is, the darker the color.
$rev = $rev_list[$k];
if ($last_rev == $rev) {
$blame_info =
($file_query->getSupportsBlameOnBlame() ?
'<th style="background: '.$color.'; width: 2em;"></th>' : '').
'<th style="background: '.$color.'; width: 9em;"></th>'.
'<th style="background: '.$color.'"></th>';
} else {
$color_number = (int)(0xEE -
0xEE * ($blame_dict[$rev]['epoch'] - $min) / $range);
$color = sprintf('#%02xee%02x', $color_number, $color_number);
$revision_link = self::renderRevision(
$drequest,
substr($rev, 0, 7));
if (!$file_query->getSupportsBlameOnBlame()) {
$prev_link = '';
} else {
$prev_rev = $file_query->getPrevRev($rev);
$path = $drequest->getPath();
$prev_link = self::renderBrowse(
$drequest,
$path,
"\xC2\xAB",
$prev_rev,
$n,
$selected);
$prev_link = '<th style="background: ' . $color .
'; width: 2em;">' . $prev_link . '</th>';
}
$author_link = $blame_dict[$rev]['author'];
$blame_info =
$prev_link .
'<th style="background: '.$color.
'; width: 12em;">'.$revision_link.'</th>'.
'<th style="background: '.$color.'; width: 12em'.
'; font-weight: normal; color: #333;">'.$author_link.'</th>';
$last_rev = $rev;
}
} else {
$blame_info = null;
}
// Highlight the line of interest if needed.
if ($n == $drequest->getLine()) {
$tr = '<tr style="background: #ffff00;">';
$targ = '<a id="scroll_target"></a>';
Javelin::initBehavior('diffusion-jump-to',
array('target' => 'scroll_target'));
} else {
$tr = '<tr>';
$targ = null;
}
// Create the row display.
$uri_path = $drequest->getUriPath();
$uri_rev = $drequest->getStableCommitName();
$uri_view = $view
? '?view='.$view
: null;
$l = phutil_render_tag(
'a',
array(
'class' => 'diffusion-line-link',
'href' => $uri_path.';'.$uri_rev.'$'.$n.$uri_view,
),
$n);
$rows[] = $tr.$blame_info.'<th>'.$l.'</th><td>'.$targ.$line.'</td></tr>';
++$n;
}
return $rows;
}
private static function renderRevision(DiffusionRequest $drequest,
$revision) {
$callsign = $drequest->getCallsign();
$name = 'r'.$callsign.$revision;
return phutil_render_tag(
'a',
array(
'href' => '/'.$name,
),
$name
);
}
private static function renderBrowse(
DiffusionRequest $drequest,
$path,
$name = null,
$rev = null,
$line = null,
$view = null) {
$callsign = $drequest->getCallsign();
if ($name === null) {
$name = $path;
}
$at = null;
if ($rev) {
$at = ';'.$rev;
}
if ($view) {
$view = '?view='.$view;
}
if ($line) {
$line = '$'.$line;
}
return phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/browse/{$path}{$at}{$line}{$view}",
),
$name
);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 14, 6:01 AM (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
336822
Default Alt Text
(52 KB)

Event Timeline