Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2890088
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
22 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/differential/controller/DifferentialRevisionLandController.php b/src/applications/differential/controller/DifferentialRevisionLandController.php
index 6bb58e5f8c..aecc5644b7 100644
--- a/src/applications/differential/controller/DifferentialRevisionLandController.php
+++ b/src/applications/differential/controller/DifferentialRevisionLandController.php
@@ -1,134 +1,154 @@
<?php
final class DifferentialRevisionLandController extends DifferentialController {
private $revisionID;
private $strategyClass;
private $pushStrategy;
public function willProcessRequest(array $data) {
$this->revisionID = $data['id'];
$this->strategyClass = $data['strategy'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$revision_id = $this->revisionID;
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($revision_id))
->setViewer($viewer)
->executeOne();
if (!$revision) {
return new Aphront404Response();
}
if (is_subclass_of($this->strategyClass, 'DifferentialLandingStrategy')) {
$this->pushStrategy = newv($this->strategyClass, array());
} else {
throw new Exception(
"Strategy type must be a valid class name and must subclass ".
"DifferentialLandingStrategy. ".
"'{$this->strategyClass}' is not a subclass of ".
"DifferentialLandingStrategy.");
}
if ($request->isDialogFormPost()) {
$response = null;
$text = '';
try {
$response = $this->attemptLand($revision, $request);
$title = pht("Success!");
$text = pht("Revision was successfully landed.");
} catch (Exception $ex) {
$title = pht("Failed to land revision");
if ($ex instanceof PhutilProxyException) {
$text = hsprintf(
'%s:<br><pre>%s</pre>',
$ex->getMessage(),
$ex->getPreviousException()->getMessage());
} else {
$text = phutil_tag('pre', array(), $ex->getMessage());
}
$text = id(new AphrontErrorView())
->appendChild($text);
}
if ($response instanceof AphrontDialogView) {
$dialog = $response;
} else {
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle($title)
->appendChild(phutil_tag('p', array(), $text))
->addCancelButton('/D'.$revision_id, pht('Done'));
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
+ $is_disabled = $this->pushStrategy->isActionDisabled(
+ $viewer,
+ $revision,
+ $revision->getRepository());
+ if ($is_disabled) {
+ if (is_string($is_disabled)) {
+ $explain = $is_disabled;
+ } else {
+ $explain = pht("This action is not currently enabled.");
+ }
+ $dialog = id(new AphrontDialogView())
+ ->setUser($viewer)
+ ->setTitle(pht("Can't land revision"))
+ ->appendChild($explain)
+ ->addCancelButton('/D'.$revision_id);
+
+ return id(new AphrontDialogResponse())->setDialog($dialog);
+ }
+
+
$prompt = hsprintf('%s<br><br>%s',
pht(
'This will squash and rebase revision %s, and push it to '.
'the default / master branch.',
$revision_id),
pht('It is an experimental feature and may not work.'));
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht("Land Revision %s?", $revision_id))
->appendChild($prompt)
->setSubmitURI($request->getRequestURI())
->addSubmitButton(pht('Land it!'))
->addCancelButton('/D'.$revision_id);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function attemptLand($revision, $request) {
$status = $revision->getStatus();
if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
throw new Exception("Only Accepted revisions can be landed.");
}
$repository = $revision->getRepository();
if ($repository === null) {
throw new Exception("revision is not attached to a repository.");
}
$can_push = PhabricatorPolicyFilter::hasCapability(
$request->getUser(),
$repository,
DiffusionCapabilityPush::CAPABILITY);
if (!$can_push) {
throw new Exception(
pht('You do not have permission to push to this repository.'));
}
$lock = $this->lockRepository($repository);
try {
$response = $this->pushStrategy->processLandRequest(
$request,
$revision,
$repository);
} catch (Exception $e) {
$lock->unlock();
throw $e;
}
$lock->unlock();
return $response;
}
private function lockRepository($repository) {
$lock_name = __CLASS__.':'.($repository->getCallsign());
$lock = PhabricatorGlobalLock::newLock($lock_name);
$lock->lock();
return $lock;
}
}
diff --git a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php
index fdfea16ae0..f2fb2b16c1 100644
--- a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php
+++ b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php
@@ -1,54 +1,55 @@
<?php
+/**
+ * This class adds a "Land this" button to revision view.
+ */
final class DifferentialLandingActionMenuEventListener
extends PhabricatorEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
$this->handleActionsEvent($event);
break;
}
}
private function handleActionsEvent(PhutilEvent $event) {
$object = $event->getValue('object');
-
- $actions = null;
if ($object instanceof DifferentialRevision) {
- $actions = $this->renderRevisionAction($event);
+ $this->renderRevisionAction($event);
}
-
- $this->addActionMenuItems($event, $actions);
}
private function renderRevisionAction(PhutilEvent $event) {
if (!$this->canUseApplication($event->getUser())) {
return null;
}
$revision = $event->getValue('object');
$repository = $revision->getRepository();
if ($repository === null) {
return null;
}
$strategies = id(new PhutilSymbolLoader())
->setAncestorClass('DifferentialLandingStrategy')
->loadObjects();
foreach ($strategies as $strategy) {
- $actions = $strategy->createMenuItems(
- $event->getUser(),
- $revision,
- $repository);
- $this->addActionMenuItems($event, $actions);
+ $viewer = $event->getUser();
+ $action = $strategy->createMenuItem($viewer, $revision, $repository);
+ if ($action == null)
+ continue;
+ if ($strategy->isActionDisabled($viewer, $revision, $repository)) {
+ $action->setDisabled(true);
+ }
+ $this->addActionMenuItems($event, $action);
}
}
}
-
diff --git a/src/applications/differential/landing/DifferentialLandingStrategy.php b/src/applications/differential/landing/DifferentialLandingStrategy.php
index 0c7efad5f0..aea5d83719 100644
--- a/src/applications/differential/landing/DifferentialLandingStrategy.php
+++ b/src/applications/differential/landing/DifferentialLandingStrategy.php
@@ -1,57 +1,86 @@
<?php
abstract class DifferentialLandingStrategy {
public abstract function processLandRequest(
AphrontRequest $request,
DifferentialRevision $revision,
PhabricatorRepository $repository);
/**
- * returns PhabricatorActionView or an array of PhabricatorActionView or null.
+ * returns PhabricatorActionView or null.
*/
- abstract function createMenuItems(
+ abstract function createMenuItem(
PhabricatorUser $viewer,
DifferentialRevision $revision,
PhabricatorRepository $repository);
/**
* returns PhabricatorActionView which can be attached to the revision view.
*/
- protected function createActionView($revision, $name, $disabled = false) {
+ protected function createActionView($revision, $name) {
$strategy = get_class($this);
$revision_id = $revision->getId();
return id(new PhabricatorActionView())
->setRenderAsForm(true)
+ ->setWorkflow(true)
->setName($name)
- ->setHref("/differential/revision/land/{$revision_id}/{$strategy}/")
- ->setDisabled($disabled);
+ ->setHref("/differential/revision/land/{$revision_id}/{$strategy}/");
+ }
+
+ /**
+ * Check if this action should be disabled, and explain why.
+ *
+ * By default, this method checks for push permissions, and for the
+ * revision being Accepted.
+ *
+ * @return FALSE for "not disabled";
+ * Human-readable text explaining why, if it is disabled;
+ */
+ public function isActionDisabled(
+ PhabricatorUser $viewer,
+ DifferentialRevision $revision,
+ PhabricatorRepository $repository) {
+
+ $status = $revision->getStatus();
+ if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
+ return pht("Only Accepted revisions can be landed.");
+ }
+
+ if (!PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $repository,
+ DiffusionCapabilityPush::CAPABILITY)) {
+ return pht("You do not have permissions to push to this repository.");
+ }
+
+ return false;
}
/**
* might break if repository is not Git.
*/
protected function getGitWorkspace(PhabricatorRepository $repository) {
try {
return DifferentialGetWorkingCopy::getCleanGitWorkspace($repository);
} catch (Exception $e) {
throw new PhutilProxyException(
'Failed to allocate a workspace',
$e);
}
}
/**
* might break if repository is not Mercurial.
*/
protected function getMercurialWorkspace(PhabricatorRepository $repository) {
try {
return DifferentialGetWorkingCopy::getCleanMercurialWorkspace(
$repository);
} catch (Exception $e) {
throw new PhutilProxyException(
'Failed to allocate a workspace',
$e);
}
}
}
diff --git a/src/applications/differential/landing/DifferentialLandingToGitHub.php b/src/applications/differential/landing/DifferentialLandingToGitHub.php
index 7eb19ecf92..00c5362b22 100644
--- a/src/applications/differential/landing/DifferentialLandingToGitHub.php
+++ b/src/applications/differential/landing/DifferentialLandingToGitHub.php
@@ -1,178 +1,178 @@
<?php
final class DifferentialLandingToGitHub
extends DifferentialLandingStrategy {
private $account;
private $provider;
public function processLandRequest(
AphrontRequest $request,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$viewer = $request->getUser();
$this->init($viewer, $repository);
$workspace = $this->getGitWorkspace($repository);
try {
id(new DifferentialLandingToHostedGit())
->commitRevisionToWorkspace(
$revision,
$workspace,
$viewer);
} catch (Exception $e) {
throw new PhutilProxyException(
'Failed to commit patch',
$e);
}
try {
$this->pushWorkspaceRepository($repository, $workspace);
} catch (Exception $e) {
// If it's a permission problem, we know more than git.
$dialog = $this->verifyRemotePermissions($viewer, $revision, $repository);
if ($dialog) {
return $dialog;
}
// Else, throw what git said.
throw new PhutilProxyException(
'Failed to push changes upstream',
$e);
}
}
/**
* returns PhabricatorActionView or an array of PhabricatorActionView or null.
*/
- public function createMenuItems(
+ public function createMenuItem(
PhabricatorUser $viewer,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$vcs = $repository->getVersionControlSystem();
if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
return;
}
if ($repository->isHosted()) {
return;
}
try {
// These throw when failing.
$this->init($viewer, $repository);
$this->findGitHubRepo($repository);
} catch (Exception $e) {
return;
}
return $this->createActionView($revision, pht('Land to GitHub'))
->setIcon('octocat');
}
public function pushWorkspaceRepository(
PhabricatorRepository $repository,
ArcanistRepositoryAPI $workspace) {
$token = $this->getAccessToken();
$github_repo = $this->findGitHubRepo($repository);
$remote = urisprintf(
'https://%s:x-oauth-basic@%s/%s.git',
$token,
$this->provider->getProviderDomain(),
$github_repo);
$workspace->execxLocal(
"push %P HEAD:master",
new PhutilOpaqueEnvelope($remote));
}
private function init($viewer, $repository) {
$repo_uri = $repository->getRemoteURIObject();
$repo_domain = $repo_uri->getDomain();
$this->account = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withAccountTypes(array("github"))
->withAccountDomains(array($repo_domain))
->executeOne();
if (!$this->account) {
throw new Exception(
"No matching GitHub account found for {$repo_domain}.");
}
$this->provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$this->account->getProviderKey());
if (!$this->provider) {
throw new Exception("GitHub provider for {$repo_domain} is not enabled.");
}
}
private function findGitHubRepo(PhabricatorRepository $repository) {
$repo_uri = $repository->getRemoteURIObject();
$repo_path = $repo_uri->getPath();
if (substr($repo_path, -4) == '.git') {
$repo_path = substr($repo_path, 0, -4);
}
$repo_path = ltrim($repo_path, '/');
return $repo_path;
}
private function getAccessToken() {
return $this->provider->getOAuthAccessToken($this->account);
}
private function verifyRemotePermissions($viewer, $revision, $repository) {
$github_user = $this->account->getUsername();
$github_repo = $this->findGitHubRepo($repository);
$uri = urisprintf(
'https://api.github.com/repos/%s/collaborators/%s',
$github_repo,
$github_user);
$uri = new PhutilURI($uri);
$uri->setQueryParam('access_token', $this->getAccessToken());
list($status, $body, $headers) = id(new HTTPSFuture($uri))->resolve();
// Likely status codes:
// 204 No Content: Has permissions. Token might be too weak.
// 404 Not Found: Not a collaborator.
// 401 Unauthorized: Token is bad/revoked.
$no_permission = ($status->getStatusCode() == 404);
if ($no_permission) {
throw new Exception(
"You don't have permission to push to this repository. \n".
"Push permissions for this repository are managed on GitHub.");
}
$scopes = BaseHTTPFuture::getHeader($headers, 'X-OAuth-Scopes');
if (strpos($scopes, 'public_repo') === false) {
$provider_key = $this->provider->getProviderKey();
$refresh_token_uri = new PhutilURI("/auth/refresh/{$provider_key}/");
$refresh_token_uri->setQueryParam('scope', 'public_repo');
return id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Stronger token needed'))
->appendChild(pht(
'In order to complete this action, you need a '.
'stronger GitHub token.'))
->setSubmitURI($refresh_token_uri)
->addCancelButton('/D'.$revision->getId())
->addSubmitButton(pht('Refresh Account Link'));
}
}
}
diff --git a/src/applications/differential/landing/DifferentialLandingToHostedGit.php b/src/applications/differential/landing/DifferentialLandingToHostedGit.php
index 874b0aff2c..8438492aba 100644
--- a/src/applications/differential/landing/DifferentialLandingToHostedGit.php
+++ b/src/applications/differential/landing/DifferentialLandingToHostedGit.php
@@ -1,136 +1,130 @@
<?php
final class DifferentialLandingToHostedGit
extends DifferentialLandingStrategy {
public function processLandRequest(
AphrontRequest $request,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$viewer = $request->getUser();
$workspace = $this->getGitWorkspace($repository);
try {
$this->commitRevisionToWorkspace(
$revision,
$workspace,
$viewer);
} catch (Exception $e) {
throw new PhutilProxyException(
'Failed to commit patch',
$e);
}
try {
$this->pushWorkspaceRepository(
$repository,
$workspace,
$viewer);
} catch (Exception $e) {
throw new PhutilProxyException(
'Failed to push changes upstream',
$e);
}
}
public function commitRevisionToWorkspace(
DifferentialRevision $revision,
ArcanistRepositoryAPI $workspace,
PhabricatorUser $user) {
$diff_id = $revision->loadActiveDiff()->getID();
$call = new ConduitCall(
'differential.getrawdiff',
array(
'diffID' => $diff_id,
));
$call->setUser($user);
$raw_diff = $call->execute();
$missing_binary =
"\nindex "
. "0000000000000000000000000000000000000000.."
. "0000000000000000000000000000000000000000\n";
if (strpos($raw_diff, $missing_binary) !== false) {
throw new Exception("Patch is missing content for a binary file");
}
$future = $workspace->execFutureLocal('apply --index -');
$future->write($raw_diff);
$future->resolvex();
$workspace->reloadWorkingCopy();
$call = new ConduitCall(
'differential.getcommitmessage',
array(
'revision_id' => $revision->getID(),
));
$call->setUser($user);
$message = $call->execute();
$author = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$revision->getAuthorPHID());
$author_string = sprintf(
'%s <%s>',
$author->getRealName(),
$author->loadPrimaryEmailAddress());
$author_date = $revision->getDateCreated();
$workspace->execxLocal(
'-c user.name=%s -c user.email=%s ' .
'commit --date=%s --author=%s '.
'--message=%s',
// -c will set the 'committer'
$user->getRealName(),
$user->loadPrimaryEmailAddress(),
$author_date,
$author_string,
$message);
}
public function pushWorkspaceRepository(
PhabricatorRepository $repository,
ArcanistRepositoryAPI $workspace,
PhabricatorUser $user) {
$workspace->execxLocal("push origin HEAD:master");
}
- public function createMenuItems(
+ public function createMenuItem(
PhabricatorUser $viewer,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$vcs = $repository->getVersionControlSystem();
if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
return;
}
if (!$repository->isHosted()) {
return;
}
if (!$repository->isWorkingCopyBare()) {
return;
}
- $can_push = PhabricatorPolicyFilter::hasCapability(
- $viewer,
- $repository,
- DiffusionCapabilityPush::CAPABILITY);
-
return $this->createActionView(
$revision,
- pht('Land to Hosted Repository'),
- !$can_push);
+ pht('Land to Hosted Repository'));
}
}
diff --git a/src/applications/differential/landing/DifferentialLandingToHostedMercurial.php b/src/applications/differential/landing/DifferentialLandingToHostedMercurial.php
index fd533aaf8b..029f821814 100644
--- a/src/applications/differential/landing/DifferentialLandingToHostedMercurial.php
+++ b/src/applications/differential/landing/DifferentialLandingToHostedMercurial.php
@@ -1,120 +1,114 @@
<?php
final class DifferentialLandingToHostedMercurial
extends DifferentialLandingStrategy {
public function processLandRequest(
AphrontRequest $request,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$viewer = $request->getUser();
$workspace = $this->getMercurialWorkspace($repository);
try {
$this->commitRevisionToWorkspace(
$revision,
$workspace,
$viewer);
} catch (Exception $e) {
throw new PhutilProxyException(
'Failed to commit patch',
$e);
}
try {
$this->pushWorkspaceRepository(
$repository,
$workspace,
$viewer);
} catch (Exception $e) {
throw new PhutilProxyException(
'Failed to push changes upstream',
$e);
}
}
public function commitRevisionToWorkspace(
DifferentialRevision $revision,
ArcanistRepositoryAPI $workspace,
PhabricatorUser $user) {
$diff_id = $revision->loadActiveDiff()->getID();
$call = new ConduitCall(
'differential.getrawdiff',
array(
'diffID' => $diff_id,
));
$call->setUser($user);
$raw_diff = $call->execute();
$future = $workspace->execFutureLocal('patch --no-commit -');
$future->write($raw_diff);
$future->resolvex();
$workspace->reloadWorkingCopy();
$call = new ConduitCall(
'differential.getcommitmessage',
array(
'revision_id' => $revision->getID(),
));
$call->setUser($user);
$message = $call->execute();
$author = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$revision->getAuthorPHID());
$author_string = sprintf(
'%s <%s>',
$author->getRealName(),
$author->loadPrimaryEmailAddress());
$author_date = $revision->getDateCreated();
$workspace->execxLocal(
'commit --date=%s --user=%s '.
'--message=%s',
$author_date.' 0',
$author_string,
$message);
}
public function pushWorkspaceRepository(
PhabricatorRepository $repository,
ArcanistRepositoryAPI $workspace,
PhabricatorUser $user) {
$workspace->execxLocal("push -b default");
}
- public function createMenuItems(
+ public function createMenuItem(
PhabricatorUser $viewer,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$vcs = $repository->getVersionControlSystem();
if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL) {
return;
}
if (!$repository->isHosted()) {
return;
}
- $can_push = PhabricatorPolicyFilter::hasCapability(
- $viewer,
- $repository,
- DiffusionCapabilityPush::CAPABILITY);
-
return $this->createActionView(
$revision,
- pht('Land to Hosted Repository'),
- !$can_push);
+ pht('Land to Hosted Repository'));
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 13:03 (3 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1124870
Default Alt Text
(22 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment