mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-30 01:10:58 +01:00
Land to GitHub + support stuff
Summary: A usable, Land to GitHub flow. Still to do: - Refactor all git/hg stratagies to a sane structure. - Make the dialogs Workflow + explain why it's disabled. - Show button and request Link Account if GH is enabled, but user is not linked. - After refreshing token, user ends up in the settings stage. Hacked something in LandController to be able to show an arbitrary dialog from a strategy. It's not very nice, but I want to make some more refactoring to the controller/strategy/ies anyway. Also made PhabricatorRepository::getRemoteURIObject() public, because it was very useful in getting the domain and path for the repo. Test Plan: Went through these flows: - load revision in hosted, github-backed, non-github backed repos to see button as needed. - hit land with weak token - sent to refresh it with the extra scope. - Land to repo I'm not allowed - got proper error message. - Successfully landed; Failed to apply patch. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley CC: Korvin, epriestley, aran Maniphest Tasks: T182 Differential Revision: https://secure.phabricator.com/D7555
This commit is contained in:
parent
3b257381ad
commit
dcf909ba56
6 changed files with 247 additions and 57 deletions
|
@ -857,7 +857,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'aphront-dialog-view-css' =>
|
||||
array(
|
||||
'uri' => '/res/6b6a41c6/rsrc/css/aphront/dialog-view.css',
|
||||
'uri' => '/res/8f151d2a/rsrc/css/aphront/dialog-view.css',
|
||||
'type' => 'css',
|
||||
'requires' =>
|
||||
array(
|
||||
|
@ -4328,7 +4328,7 @@ celerity_register_resource_map(array(
|
|||
), array(
|
||||
'packages' =>
|
||||
array(
|
||||
'd831cac3' =>
|
||||
'1a71c1b4' =>
|
||||
array(
|
||||
'name' => 'core.pkg.css',
|
||||
'symbols' =>
|
||||
|
@ -4377,7 +4377,7 @@ celerity_register_resource_map(array(
|
|||
41 => 'phabricator-tag-view-css',
|
||||
42 => 'phui-list-view-css',
|
||||
),
|
||||
'uri' => '/res/pkg/d831cac3/core.pkg.css',
|
||||
'uri' => '/res/pkg/1a71c1b4/core.pkg.css',
|
||||
'type' => 'css',
|
||||
),
|
||||
'2c1dba03' =>
|
||||
|
@ -4569,15 +4569,15 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'reverse' =>
|
||||
array(
|
||||
'aphront-dialog-view-css' => 'd831cac3',
|
||||
'aphront-error-view-css' => 'd831cac3',
|
||||
'aphront-list-filter-view-css' => 'd831cac3',
|
||||
'aphront-pager-view-css' => 'd831cac3',
|
||||
'aphront-panel-view-css' => 'd831cac3',
|
||||
'aphront-table-view-css' => 'd831cac3',
|
||||
'aphront-tokenizer-control-css' => 'd831cac3',
|
||||
'aphront-tooltip-css' => 'd831cac3',
|
||||
'aphront-typeahead-control-css' => 'd831cac3',
|
||||
'aphront-dialog-view-css' => '1a71c1b4',
|
||||
'aphront-error-view-css' => '1a71c1b4',
|
||||
'aphront-list-filter-view-css' => '1a71c1b4',
|
||||
'aphront-pager-view-css' => '1a71c1b4',
|
||||
'aphront-panel-view-css' => '1a71c1b4',
|
||||
'aphront-table-view-css' => '1a71c1b4',
|
||||
'aphront-tokenizer-control-css' => '1a71c1b4',
|
||||
'aphront-tooltip-css' => '1a71c1b4',
|
||||
'aphront-typeahead-control-css' => '1a71c1b4',
|
||||
'differential-changeset-view-css' => '1084b12b',
|
||||
'differential-core-view-css' => '1084b12b',
|
||||
'differential-inline-comment-editor' => '5e9e5c4e',
|
||||
|
@ -4591,7 +4591,7 @@ celerity_register_resource_map(array(
|
|||
'differential-table-of-contents-css' => '1084b12b',
|
||||
'diffusion-commit-view-css' => '7aa115b4',
|
||||
'diffusion-icons-css' => '7aa115b4',
|
||||
'global-drag-and-drop-css' => 'd831cac3',
|
||||
'global-drag-and-drop-css' => '1a71c1b4',
|
||||
'inline-comment-summary-css' => '1084b12b',
|
||||
'javelin-aphlict' => '2c1dba03',
|
||||
'javelin-behavior' => '3e3be199',
|
||||
|
@ -4666,56 +4666,56 @@ celerity_register_resource_map(array(
|
|||
'javelin-util' => '3e3be199',
|
||||
'javelin-vector' => '3e3be199',
|
||||
'javelin-workflow' => '3e3be199',
|
||||
'lightbox-attachment-css' => 'd831cac3',
|
||||
'lightbox-attachment-css' => '1a71c1b4',
|
||||
'maniphest-task-summary-css' => '49898640',
|
||||
'phabricator-action-list-view-css' => 'd831cac3',
|
||||
'phabricator-application-launch-view-css' => 'd831cac3',
|
||||
'phabricator-action-list-view-css' => '1a71c1b4',
|
||||
'phabricator-application-launch-view-css' => '1a71c1b4',
|
||||
'phabricator-busy' => '2c1dba03',
|
||||
'phabricator-content-source-view-css' => '1084b12b',
|
||||
'phabricator-core-css' => 'd831cac3',
|
||||
'phabricator-crumbs-view-css' => 'd831cac3',
|
||||
'phabricator-core-css' => '1a71c1b4',
|
||||
'phabricator-crumbs-view-css' => '1a71c1b4',
|
||||
'phabricator-drag-and-drop-file-upload' => '5e9e5c4e',
|
||||
'phabricator-dropdown-menu' => '2c1dba03',
|
||||
'phabricator-file-upload' => '2c1dba03',
|
||||
'phabricator-filetree-view-css' => 'd831cac3',
|
||||
'phabricator-flag-css' => 'd831cac3',
|
||||
'phabricator-filetree-view-css' => '1a71c1b4',
|
||||
'phabricator-flag-css' => '1a71c1b4',
|
||||
'phabricator-hovercard' => '2c1dba03',
|
||||
'phabricator-jump-nav' => 'd831cac3',
|
||||
'phabricator-jump-nav' => '1a71c1b4',
|
||||
'phabricator-keyboard-shortcut' => '2c1dba03',
|
||||
'phabricator-keyboard-shortcut-manager' => '2c1dba03',
|
||||
'phabricator-main-menu-view' => 'd831cac3',
|
||||
'phabricator-main-menu-view' => '1a71c1b4',
|
||||
'phabricator-menu-item' => '2c1dba03',
|
||||
'phabricator-nav-view-css' => 'd831cac3',
|
||||
'phabricator-nav-view-css' => '1a71c1b4',
|
||||
'phabricator-notification' => '2c1dba03',
|
||||
'phabricator-notification-css' => 'd831cac3',
|
||||
'phabricator-notification-menu-css' => 'd831cac3',
|
||||
'phabricator-notification-css' => '1a71c1b4',
|
||||
'phabricator-notification-menu-css' => '1a71c1b4',
|
||||
'phabricator-object-selector-css' => '1084b12b',
|
||||
'phabricator-phtize' => '2c1dba03',
|
||||
'phabricator-prefab' => '2c1dba03',
|
||||
'phabricator-project-tag-css' => '49898640',
|
||||
'phabricator-remarkup-css' => 'd831cac3',
|
||||
'phabricator-remarkup-css' => '1a71c1b4',
|
||||
'phabricator-shaped-request' => '5e9e5c4e',
|
||||
'phabricator-side-menu-view-css' => 'd831cac3',
|
||||
'phabricator-standard-page-view' => 'd831cac3',
|
||||
'phabricator-tag-view-css' => 'd831cac3',
|
||||
'phabricator-side-menu-view-css' => '1a71c1b4',
|
||||
'phabricator-standard-page-view' => '1a71c1b4',
|
||||
'phabricator-tag-view-css' => '1a71c1b4',
|
||||
'phabricator-textareautils' => '2c1dba03',
|
||||
'phabricator-tooltip' => '2c1dba03',
|
||||
'phabricator-transaction-view-css' => 'd831cac3',
|
||||
'phabricator-zindex-css' => 'd831cac3',
|
||||
'phui-button-css' => 'd831cac3',
|
||||
'phui-form-css' => 'd831cac3',
|
||||
'phui-form-view-css' => 'd831cac3',
|
||||
'phui-header-view-css' => 'd831cac3',
|
||||
'phui-icon-view-css' => 'd831cac3',
|
||||
'phui-list-view-css' => 'd831cac3',
|
||||
'phui-object-item-list-view-css' => 'd831cac3',
|
||||
'phui-property-list-view-css' => 'd831cac3',
|
||||
'phui-spacing-css' => 'd831cac3',
|
||||
'sprite-apps-large-css' => 'd831cac3',
|
||||
'sprite-gradient-css' => 'd831cac3',
|
||||
'sprite-icons-css' => 'd831cac3',
|
||||
'sprite-menu-css' => 'd831cac3',
|
||||
'sprite-status-css' => 'd831cac3',
|
||||
'syntax-highlighting-css' => 'd831cac3',
|
||||
'phabricator-transaction-view-css' => '1a71c1b4',
|
||||
'phabricator-zindex-css' => '1a71c1b4',
|
||||
'phui-button-css' => '1a71c1b4',
|
||||
'phui-form-css' => '1a71c1b4',
|
||||
'phui-form-view-css' => '1a71c1b4',
|
||||
'phui-header-view-css' => '1a71c1b4',
|
||||
'phui-icon-view-css' => '1a71c1b4',
|
||||
'phui-list-view-css' => '1a71c1b4',
|
||||
'phui-object-item-list-view-css' => '1a71c1b4',
|
||||
'phui-property-list-view-css' => '1a71c1b4',
|
||||
'phui-spacing-css' => '1a71c1b4',
|
||||
'sprite-apps-large-css' => '1a71c1b4',
|
||||
'sprite-gradient-css' => '1a71c1b4',
|
||||
'sprite-icons-css' => '1a71c1b4',
|
||||
'sprite-menu-css' => '1a71c1b4',
|
||||
'sprite-status-css' => '1a71c1b4',
|
||||
'syntax-highlighting-css' => '1a71c1b4',
|
||||
),
|
||||
));
|
||||
|
|
|
@ -387,6 +387,7 @@ phutil_register_library_map(array(
|
|||
'DifferentialJIRAIssuesFieldSpecification' => 'applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php',
|
||||
'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php',
|
||||
'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php',
|
||||
'DifferentialLandingToGitHub' => 'applications/differential/landing/DifferentialLandingToGitHub.php',
|
||||
'DifferentialLandingToHostedGit' => 'applications/differential/landing/DifferentialLandingToHostedGit.php',
|
||||
'DifferentialLandingToHostedMercurial' => 'applications/differential/landing/DifferentialLandingToHostedMercurial.php',
|
||||
'DifferentialLinesFieldSpecification' => 'applications/differential/field/specification/DifferentialLinesFieldSpecification.php',
|
||||
|
@ -2675,6 +2676,7 @@ phutil_register_library_map(array(
|
|||
'DifferentialInlineCommentView' => 'AphrontView',
|
||||
'DifferentialJIRAIssuesFieldSpecification' => 'DifferentialFieldSpecification',
|
||||
'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener',
|
||||
'DifferentialLandingToGitHub' => 'DifferentialLandingStrategy',
|
||||
'DifferentialLandingToHostedGit' => 'DifferentialLandingStrategy',
|
||||
'DifferentialLandingToHostedMercurial' => 'DifferentialLandingStrategy',
|
||||
'DifferentialLinesFieldSpecification' => 'DifferentialFieldSpecification',
|
||||
|
|
|
@ -37,6 +37,11 @@ abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider {
|
|||
$adapter = $this->getAdapter();
|
||||
$adapter->setState(PhabricatorHash::digest($request->getCookie('phcid')));
|
||||
|
||||
$scope = $request->getStr("scope");
|
||||
if ($scope) {
|
||||
$adapter->setScope($scope);
|
||||
}
|
||||
|
||||
$attributes = array(
|
||||
'method' => 'GET',
|
||||
'uri' => $adapter->getAuthenticateURI(),
|
||||
|
|
|
@ -36,13 +36,14 @@ final class DifferentialRevisionLandController extends DifferentialController {
|
|||
}
|
||||
|
||||
if ($request->isDialogFormPost()) {
|
||||
$response = null;
|
||||
$text = '';
|
||||
try {
|
||||
$this->attemptLand($revision, $request);
|
||||
$response = $this->attemptLand($revision, $request);
|
||||
$title = pht("Success!");
|
||||
$text = pht("Revision was successfully landed.");
|
||||
} catch (Exception $ex) {
|
||||
$title = pht("Failed to land revision");
|
||||
$text = 'moo';
|
||||
if ($ex instanceof PhutilProxyException) {
|
||||
$text = hsprintf(
|
||||
'%s:<br><pre>%s</pre>',
|
||||
|
@ -55,13 +56,15 @@ final class DifferentialRevisionLandController extends DifferentialController {
|
|||
->appendChild($text);
|
||||
}
|
||||
|
||||
$dialog = id(new AphrontDialogView())
|
||||
->setUser($viewer)
|
||||
->setTitle($title)
|
||||
->appendChild(phutil_tag('p', array(), $text))
|
||||
->setSubmitURI('/D'.$revision_id)
|
||||
->addSubmitButton(pht('Done'));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -108,7 +111,7 @@ final class DifferentialRevisionLandController extends DifferentialController {
|
|||
$lock = $this->lockRepository($repository);
|
||||
|
||||
try {
|
||||
$this->pushStrategy->processLandRequest(
|
||||
$response = $this->pushStrategy->processLandRequest(
|
||||
$request,
|
||||
$revision,
|
||||
$repository);
|
||||
|
@ -118,6 +121,7 @@ final class DifferentialRevisionLandController extends DifferentialController {
|
|||
}
|
||||
|
||||
$lock->unlock();
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function lockRepository($repository) {
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
<?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(
|
||||
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'));
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -577,7 +577,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
|||
* @{class@libphutil:PhutilGitURI}.
|
||||
* @task uri
|
||||
*/
|
||||
private function getRemoteURIObject() {
|
||||
public function getRemoteURIObject() {
|
||||
$raw_uri = $this->getDetail('remote-uri');
|
||||
if (!$raw_uri) {
|
||||
return new PhutilURI('');
|
||||
|
|
Loading…
Reference in a new issue