Index: src/__phutil_library_map__.php
===================================================================
--- src/__phutil_library_map__.php
+++ src/__phutil_library_map__.php
@@ -387,6 +387,7 @@
'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',
@@ -2660,6 +2661,7 @@
'DifferentialInlineCommentView' => 'AphrontView',
'DifferentialJIRAIssuesFieldSpecification' => 'DifferentialFieldSpecification',
'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener',
+ 'DifferentialLandingToGitHub' => 'DifferentialLandingStrategy',
'DifferentialLandingToHostedGit' => 'DifferentialLandingStrategy',
'DifferentialLandingToHostedMercurial' => 'DifferentialLandingStrategy',
'DifferentialLinesFieldSpecification' => 'DifferentialFieldSpecification',
Index: src/applications/auth/provider/PhabricatorAuthProviderOAuth.php
===================================================================
--- src/applications/auth/provider/PhabricatorAuthProviderOAuth.php
+++ src/applications/auth/provider/PhabricatorAuthProviderOAuth.php
@@ -37,6 +37,11 @@
$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(),
Index: src/applications/auth/provider/PhabricatorAuthProviderOAuthGitHub.php
===================================================================
--- src/applications/auth/provider/PhabricatorAuthProviderOAuthGitHub.php
+++ src/applications/auth/provider/PhabricatorAuthProviderOAuthGitHub.php
@@ -41,4 +41,16 @@
return PhabricatorEnv::getURI('/oauth/github/login/');
}
+ public static function getGitHubProvider() {
+ $providers = self::getAllEnabledProviders();
+
+ foreach ($providers as $provider) {
+ if ($provider instanceof PhabricatorAuthProviderOAuthGitHub) {
+ return $provider;
+ }
+ }
+
+ return null;
+ }
+
}
Index: src/applications/differential/controller/DifferentialRevisionLandController.php
===================================================================
--- src/applications/differential/controller/DifferentialRevisionLandController.php
+++ src/applications/differential/controller/DifferentialRevisionLandController.php
@@ -36,13 +36,14 @@
}
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:
%s', @@ -55,13 +56,15 @@ ->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); } @@ -78,7 +81,7 @@ ->appendChild($prompt) ->setSubmitURI($request->getRequestURI()) ->addSubmitButton(pht('Land it!')) - ->addCancelButton('/D'.$revision_id); + ->addCancelButton('/D'.$revision_id, 'Cancel'); return id(new AphrontDialogResponse())->setDialog($dialog); } @@ -108,7 +111,7 @@ $lock = $this->lockRepository($repository); try { - $this->pushStrategy->processLandRequest( + $response = $this->pushStrategy->processLandRequest( $request, $revision, $repository); @@ -118,6 +121,7 @@ } $lock->unlock(); + return $response; } private function lockRepository($repository) { Index: src/applications/differential/landing/DifferentialLandingToGitHub.php =================================================================== --- /dev/null +++ src/applications/differential/landing/DifferentialLandingToGitHub.php @@ -0,0 +1,205 @@ +getUser(); + $this->loadAccount($viewer); + + $workspace = $this->getGitWorkspace($repository); + + $this->beginCheckPermissions($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. + $permissions = $this->resolveCheckPermissions(); + + if (!$permissions['permission']) { + throw new Exception( + "You don't have permission to push to this repository. \n". + "Push permissions for this repository are managed on GitHub."); + } + if (!$permissions['good_token']) { + $domain = $this->getProvider()->getProviderDomain(); + $refresh_token_uri = new PhutilURI("/auth/refresh/github:{$domain}/"); + $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')); + } + + // 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 throws when failing. + $this->getProvider(); + $this->findGitHubRepo($repository); + } catch (Exception $e) { + return; + } + + if (!$this->loadAccount($viewer)) { + 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->getProvider()->getProviderDomain(), + $github_repo); + + list($status, $stdout, $stderr) = + $workspace->execManualLocal("push %s HEAD:master", $remote); + if (!$status) { + return; + } + + throw new CommandException( + "Command failed with error #{$status}!", + 'git push', + $status, + $this->hideToken($token, $stdout), + $this->hideToken($token, $stderr)); + } + + private function findGitHubRepo(PhabricatorRepository $repository) { + $repo_uri = $repository->getRemoteURIObject(); + + $repo_domain = $repo_uri->getDomain(); + $repo_path = $repo_uri->getPath(); + + if ($repo_domain != $this->getProvider()->getProviderDomain()) { + throw new Exception("This is not a GitHub repository."); + } + + 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->getProvider()->getOAuthAccessToken($this->account); + } + + private function getProvider() { + if (!$this->provider) { + $this->provider = PhabricatorAuthProviderOAuthGitHub::getGitHubProvider(); + } + + if (!$this->provider) { + throw new Exception("Github provider not found"); + } + return $this->provider; + } + + private function loadAccount($viewer) { + $domain = $this->getProvider()->getProviderDomain(); + + $this->account = id(new PhabricatorExternalAccountQuery()) + ->setViewer($viewer) + ->withUserPHIDs(array($viewer->getPHID())) + ->withAccountTypes(array("github")) + ->withAccountDomains(array($domain)) + ->executeOne(); + + return $this->account; + } + + private function beginCheckPermissions($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()); + $this->checkPermissionFuture = id(new HTTPSFuture($uri)); + + $this->checkPermissionFuture->start(); + } + + private function resolveCheckPermissions() { + list($status, $body, $headers) = $this->checkPermissionFuture->resolve(); + $status = $status->getStatusCode(); + $header = BaseHTTPFuture::getHeader($headers, 'X-OAuth-Scopes'); + + return array( + 'permission' => ($status == 204), + 'good_token' => (strpos($header, 'public_repo') !== false)); + } + + private function hideToken($token, $text) { + return str_replace($token, '