Index: src/__celerity_resource_map__.php =================================================================== --- src/__celerity_resource_map__.php +++ src/__celerity_resource_map__.php @@ -857,7 +857,7 @@ ), '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 @@ ), array( 'packages' => array( - 'd831cac3' => + '1a71c1b4' => array( 'name' => 'core.pkg.css', 'symbols' => @@ -4377,7 +4377,7 @@ 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 @@ ), '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 @@ '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 @@ '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', ), )); 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', @@ -2675,6 +2676,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/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); } @@ -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,179 @@ +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')); + } + } +} Index: src/applications/repository/storage/PhabricatorRepository.php =================================================================== --- src/applications/repository/storage/PhabricatorRepository.php +++ src/applications/repository/storage/PhabricatorRepository.php @@ -577,7 +577,7 @@ * @{class@libphutil:PhutilGitURI}. * @task uri */ - private function getRemoteURIObject() { + public function getRemoteURIObject() { $raw_uri = $this->getDetail('remote-uri'); if (!$raw_uri) { return new PhutilURI('');