Page MenuHomePhorge

No OneTemporary

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

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)

Event Timeline