@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

Aviv Eyal and committed by
epriestley
dcf909ba 3b257381

+247 -57
+46 -46
src/__celerity_resource_map__.php
··· 857 857 ), 858 858 'aphront-dialog-view-css' => 859 859 array( 860 - 'uri' => '/res/6b6a41c6/rsrc/css/aphront/dialog-view.css', 860 + 'uri' => '/res/8f151d2a/rsrc/css/aphront/dialog-view.css', 861 861 'type' => 'css', 862 862 'requires' => 863 863 array( ··· 4328 4328 ), array( 4329 4329 'packages' => 4330 4330 array( 4331 - 'd831cac3' => 4331 + '1a71c1b4' => 4332 4332 array( 4333 4333 'name' => 'core.pkg.css', 4334 4334 'symbols' => ··· 4377 4377 41 => 'phabricator-tag-view-css', 4378 4378 42 => 'phui-list-view-css', 4379 4379 ), 4380 - 'uri' => '/res/pkg/d831cac3/core.pkg.css', 4380 + 'uri' => '/res/pkg/1a71c1b4/core.pkg.css', 4381 4381 'type' => 'css', 4382 4382 ), 4383 4383 '2c1dba03' => ··· 4569 4569 ), 4570 4570 'reverse' => 4571 4571 array( 4572 - 'aphront-dialog-view-css' => 'd831cac3', 4573 - 'aphront-error-view-css' => 'd831cac3', 4574 - 'aphront-list-filter-view-css' => 'd831cac3', 4575 - 'aphront-pager-view-css' => 'd831cac3', 4576 - 'aphront-panel-view-css' => 'd831cac3', 4577 - 'aphront-table-view-css' => 'd831cac3', 4578 - 'aphront-tokenizer-control-css' => 'd831cac3', 4579 - 'aphront-tooltip-css' => 'd831cac3', 4580 - 'aphront-typeahead-control-css' => 'd831cac3', 4572 + 'aphront-dialog-view-css' => '1a71c1b4', 4573 + 'aphront-error-view-css' => '1a71c1b4', 4574 + 'aphront-list-filter-view-css' => '1a71c1b4', 4575 + 'aphront-pager-view-css' => '1a71c1b4', 4576 + 'aphront-panel-view-css' => '1a71c1b4', 4577 + 'aphront-table-view-css' => '1a71c1b4', 4578 + 'aphront-tokenizer-control-css' => '1a71c1b4', 4579 + 'aphront-tooltip-css' => '1a71c1b4', 4580 + 'aphront-typeahead-control-css' => '1a71c1b4', 4581 4581 'differential-changeset-view-css' => '1084b12b', 4582 4582 'differential-core-view-css' => '1084b12b', 4583 4583 'differential-inline-comment-editor' => '5e9e5c4e', ··· 4591 4591 'differential-table-of-contents-css' => '1084b12b', 4592 4592 'diffusion-commit-view-css' => '7aa115b4', 4593 4593 'diffusion-icons-css' => '7aa115b4', 4594 - 'global-drag-and-drop-css' => 'd831cac3', 4594 + 'global-drag-and-drop-css' => '1a71c1b4', 4595 4595 'inline-comment-summary-css' => '1084b12b', 4596 4596 'javelin-aphlict' => '2c1dba03', 4597 4597 'javelin-behavior' => '3e3be199', ··· 4666 4666 'javelin-util' => '3e3be199', 4667 4667 'javelin-vector' => '3e3be199', 4668 4668 'javelin-workflow' => '3e3be199', 4669 - 'lightbox-attachment-css' => 'd831cac3', 4669 + 'lightbox-attachment-css' => '1a71c1b4', 4670 4670 'maniphest-task-summary-css' => '49898640', 4671 - 'phabricator-action-list-view-css' => 'd831cac3', 4672 - 'phabricator-application-launch-view-css' => 'd831cac3', 4671 + 'phabricator-action-list-view-css' => '1a71c1b4', 4672 + 'phabricator-application-launch-view-css' => '1a71c1b4', 4673 4673 'phabricator-busy' => '2c1dba03', 4674 4674 'phabricator-content-source-view-css' => '1084b12b', 4675 - 'phabricator-core-css' => 'd831cac3', 4676 - 'phabricator-crumbs-view-css' => 'd831cac3', 4675 + 'phabricator-core-css' => '1a71c1b4', 4676 + 'phabricator-crumbs-view-css' => '1a71c1b4', 4677 4677 'phabricator-drag-and-drop-file-upload' => '5e9e5c4e', 4678 4678 'phabricator-dropdown-menu' => '2c1dba03', 4679 4679 'phabricator-file-upload' => '2c1dba03', 4680 - 'phabricator-filetree-view-css' => 'd831cac3', 4681 - 'phabricator-flag-css' => 'd831cac3', 4680 + 'phabricator-filetree-view-css' => '1a71c1b4', 4681 + 'phabricator-flag-css' => '1a71c1b4', 4682 4682 'phabricator-hovercard' => '2c1dba03', 4683 - 'phabricator-jump-nav' => 'd831cac3', 4683 + 'phabricator-jump-nav' => '1a71c1b4', 4684 4684 'phabricator-keyboard-shortcut' => '2c1dba03', 4685 4685 'phabricator-keyboard-shortcut-manager' => '2c1dba03', 4686 - 'phabricator-main-menu-view' => 'd831cac3', 4686 + 'phabricator-main-menu-view' => '1a71c1b4', 4687 4687 'phabricator-menu-item' => '2c1dba03', 4688 - 'phabricator-nav-view-css' => 'd831cac3', 4688 + 'phabricator-nav-view-css' => '1a71c1b4', 4689 4689 'phabricator-notification' => '2c1dba03', 4690 - 'phabricator-notification-css' => 'd831cac3', 4691 - 'phabricator-notification-menu-css' => 'd831cac3', 4690 + 'phabricator-notification-css' => '1a71c1b4', 4691 + 'phabricator-notification-menu-css' => '1a71c1b4', 4692 4692 'phabricator-object-selector-css' => '1084b12b', 4693 4693 'phabricator-phtize' => '2c1dba03', 4694 4694 'phabricator-prefab' => '2c1dba03', 4695 4695 'phabricator-project-tag-css' => '49898640', 4696 - 'phabricator-remarkup-css' => 'd831cac3', 4696 + 'phabricator-remarkup-css' => '1a71c1b4', 4697 4697 'phabricator-shaped-request' => '5e9e5c4e', 4698 - 'phabricator-side-menu-view-css' => 'd831cac3', 4699 - 'phabricator-standard-page-view' => 'd831cac3', 4700 - 'phabricator-tag-view-css' => 'd831cac3', 4698 + 'phabricator-side-menu-view-css' => '1a71c1b4', 4699 + 'phabricator-standard-page-view' => '1a71c1b4', 4700 + 'phabricator-tag-view-css' => '1a71c1b4', 4701 4701 'phabricator-textareautils' => '2c1dba03', 4702 4702 'phabricator-tooltip' => '2c1dba03', 4703 - 'phabricator-transaction-view-css' => 'd831cac3', 4704 - 'phabricator-zindex-css' => 'd831cac3', 4705 - 'phui-button-css' => 'd831cac3', 4706 - 'phui-form-css' => 'd831cac3', 4707 - 'phui-form-view-css' => 'd831cac3', 4708 - 'phui-header-view-css' => 'd831cac3', 4709 - 'phui-icon-view-css' => 'd831cac3', 4710 - 'phui-list-view-css' => 'd831cac3', 4711 - 'phui-object-item-list-view-css' => 'd831cac3', 4712 - 'phui-property-list-view-css' => 'd831cac3', 4713 - 'phui-spacing-css' => 'd831cac3', 4714 - 'sprite-apps-large-css' => 'd831cac3', 4715 - 'sprite-gradient-css' => 'd831cac3', 4716 - 'sprite-icons-css' => 'd831cac3', 4717 - 'sprite-menu-css' => 'd831cac3', 4718 - 'sprite-status-css' => 'd831cac3', 4719 - 'syntax-highlighting-css' => 'd831cac3', 4703 + 'phabricator-transaction-view-css' => '1a71c1b4', 4704 + 'phabricator-zindex-css' => '1a71c1b4', 4705 + 'phui-button-css' => '1a71c1b4', 4706 + 'phui-form-css' => '1a71c1b4', 4707 + 'phui-form-view-css' => '1a71c1b4', 4708 + 'phui-header-view-css' => '1a71c1b4', 4709 + 'phui-icon-view-css' => '1a71c1b4', 4710 + 'phui-list-view-css' => '1a71c1b4', 4711 + 'phui-object-item-list-view-css' => '1a71c1b4', 4712 + 'phui-property-list-view-css' => '1a71c1b4', 4713 + 'phui-spacing-css' => '1a71c1b4', 4714 + 'sprite-apps-large-css' => '1a71c1b4', 4715 + 'sprite-gradient-css' => '1a71c1b4', 4716 + 'sprite-icons-css' => '1a71c1b4', 4717 + 'sprite-menu-css' => '1a71c1b4', 4718 + 'sprite-status-css' => '1a71c1b4', 4719 + 'syntax-highlighting-css' => '1a71c1b4', 4720 4720 ), 4721 4721 ));
+2
src/__phutil_library_map__.php
··· 387 387 'DifferentialJIRAIssuesFieldSpecification' => 'applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php', 388 388 'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php', 389 389 'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php', 390 + 'DifferentialLandingToGitHub' => 'applications/differential/landing/DifferentialLandingToGitHub.php', 390 391 'DifferentialLandingToHostedGit' => 'applications/differential/landing/DifferentialLandingToHostedGit.php', 391 392 'DifferentialLandingToHostedMercurial' => 'applications/differential/landing/DifferentialLandingToHostedMercurial.php', 392 393 'DifferentialLinesFieldSpecification' => 'applications/differential/field/specification/DifferentialLinesFieldSpecification.php', ··· 2675 2676 'DifferentialInlineCommentView' => 'AphrontView', 2676 2677 'DifferentialJIRAIssuesFieldSpecification' => 'DifferentialFieldSpecification', 2677 2678 'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener', 2679 + 'DifferentialLandingToGitHub' => 'DifferentialLandingStrategy', 2678 2680 'DifferentialLandingToHostedGit' => 'DifferentialLandingStrategy', 2679 2681 'DifferentialLandingToHostedMercurial' => 'DifferentialLandingStrategy', 2680 2682 'DifferentialLinesFieldSpecification' => 'DifferentialFieldSpecification',
+5
src/applications/auth/provider/PhabricatorAuthProviderOAuth.php
··· 37 37 $adapter = $this->getAdapter(); 38 38 $adapter->setState(PhabricatorHash::digest($request->getCookie('phcid'))); 39 39 40 + $scope = $request->getStr("scope"); 41 + if ($scope) { 42 + $adapter->setScope($scope); 43 + } 44 + 40 45 $attributes = array( 41 46 'method' => 'GET', 42 47 'uri' => $adapter->getAuthenticateURI(),
+14 -10
src/applications/differential/controller/DifferentialRevisionLandController.php
··· 36 36 } 37 37 38 38 if ($request->isDialogFormPost()) { 39 + $response = null; 40 + $text = ''; 39 41 try { 40 - $this->attemptLand($revision, $request); 42 + $response = $this->attemptLand($revision, $request); 41 43 $title = pht("Success!"); 42 44 $text = pht("Revision was successfully landed."); 43 45 } catch (Exception $ex) { 44 46 $title = pht("Failed to land revision"); 45 - $text = 'moo'; 46 47 if ($ex instanceof PhutilProxyException) { 47 48 $text = hsprintf( 48 49 '%s:<br><pre>%s</pre>', ··· 55 56 ->appendChild($text); 56 57 } 57 58 58 - $dialog = id(new AphrontDialogView()) 59 - ->setUser($viewer) 60 - ->setTitle($title) 61 - ->appendChild(phutil_tag('p', array(), $text)) 62 - ->setSubmitURI('/D'.$revision_id) 63 - ->addSubmitButton(pht('Done')); 64 - 59 + if ($response instanceof AphrontDialogView) { 60 + $dialog = $response; 61 + } else { 62 + $dialog = id(new AphrontDialogView()) 63 + ->setUser($viewer) 64 + ->setTitle($title) 65 + ->appendChild(phutil_tag('p', array(), $text)) 66 + ->addCancelButton('/D'.$revision_id, pht('Done')); 67 + } 65 68 return id(new AphrontDialogResponse())->setDialog($dialog); 66 69 } 67 70 ··· 108 111 $lock = $this->lockRepository($repository); 109 112 110 113 try { 111 - $this->pushStrategy->processLandRequest( 114 + $response = $this->pushStrategy->processLandRequest( 112 115 $request, 113 116 $revision, 114 117 $repository); ··· 118 121 } 119 122 120 123 $lock->unlock(); 124 + return $response; 121 125 } 122 126 123 127 private function lockRepository($repository) {
+179
src/applications/differential/landing/DifferentialLandingToGitHub.php
··· 1 + <?php 2 + 3 + final class DifferentialLandingToGitHub 4 + extends DifferentialLandingStrategy { 5 + 6 + private $account; 7 + private $provider; 8 + 9 + public function processLandRequest( 10 + AphrontRequest $request, 11 + DifferentialRevision $revision, 12 + PhabricatorRepository $repository) { 13 + 14 + $viewer = $request->getUser(); 15 + $this->init($viewer, $repository); 16 + 17 + $workspace = $this->getGitWorkspace($repository); 18 + 19 + try { 20 + id(new DifferentialLandingToHostedGit()) 21 + ->commitRevisionToWorkspace( 22 + $revision, 23 + $workspace, 24 + $viewer); 25 + } catch (Exception $e) { 26 + throw new PhutilProxyException( 27 + 'Failed to commit patch', 28 + $e); 29 + } 30 + 31 + try { 32 + $this->pushWorkspaceRepository($repository, $workspace); 33 + } catch (Exception $e) { 34 + // If it's a permission problem, we know more than git. 35 + $dialog = $this->verifyRemotePermissions($viewer, $revision, $repository); 36 + if ($dialog) { 37 + return $dialog; 38 + } 39 + 40 + // Else, throw what git said. 41 + throw new PhutilProxyException( 42 + 'Failed to push changes upstream', 43 + $e); 44 + } 45 + } 46 + 47 + /** 48 + * returns PhabricatorActionView or an array of PhabricatorActionView or null. 49 + */ 50 + public function createMenuItems( 51 + PhabricatorUser $viewer, 52 + DifferentialRevision $revision, 53 + PhabricatorRepository $repository) { 54 + 55 + $vcs = $repository->getVersionControlSystem(); 56 + if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) { 57 + return; 58 + } 59 + 60 + if ($repository->isHosted()) { 61 + return; 62 + } 63 + 64 + try { 65 + // These throw when failing. 66 + $this->init($viewer, $repository); 67 + $this->findGitHubRepo($repository); 68 + } catch (Exception $e) { 69 + return; 70 + } 71 + 72 + return $this->createActionView( 73 + $revision, 74 + pht('Land to GitHub')); 75 + } 76 + 77 + public function pushWorkspaceRepository( 78 + PhabricatorRepository $repository, 79 + ArcanistRepositoryAPI $workspace) { 80 + 81 + $token = $this->getAccessToken(); 82 + 83 + $github_repo = $this->findGitHubRepo($repository); 84 + 85 + $remote = urisprintf( 86 + 'https://%s:x-oauth-basic@%s/%s.git', 87 + $token, 88 + $this->provider->getProviderDomain(), 89 + $github_repo); 90 + 91 + $workspace->execxLocal( 92 + "push %P HEAD:master", 93 + new PhutilOpaqueEnvelope($remote)); 94 + } 95 + 96 + private function init($viewer, $repository) { 97 + $repo_uri = $repository->getRemoteURIObject(); 98 + $repo_domain = $repo_uri->getDomain(); 99 + 100 + $this->account = id(new PhabricatorExternalAccountQuery()) 101 + ->setViewer($viewer) 102 + ->withUserPHIDs(array($viewer->getPHID())) 103 + ->withAccountTypes(array("github")) 104 + ->withAccountDomains(array($repo_domain)) 105 + ->executeOne(); 106 + 107 + if (!$this->account) { 108 + throw new Exception( 109 + "No matching GitHub account found for {$repo_domain}."); 110 + } 111 + 112 + $this->provider = PhabricatorAuthProvider::getEnabledProviderByKey( 113 + $this->account->getProviderKey()); 114 + if (!$this->provider) { 115 + throw new Exception("GitHub provider for {$repo_domain} is not enabled."); 116 + } 117 + } 118 + 119 + private function findGitHubRepo(PhabricatorRepository $repository) { 120 + $repo_uri = $repository->getRemoteURIObject(); 121 + 122 + $repo_path = $repo_uri->getPath(); 123 + 124 + if (substr($repo_path, -4) == '.git') { 125 + $repo_path = substr($repo_path, 0, -4); 126 + } 127 + $repo_path = ltrim($repo_path, '/'); 128 + 129 + return $repo_path; 130 + } 131 + 132 + private function getAccessToken() { 133 + return $this->provider->getOAuthAccessToken($this->account); 134 + } 135 + 136 + private function verifyRemotePermissions($viewer, $revision, $repository) { 137 + $github_user = $this->account->getUsername(); 138 + $github_repo = $this->findGitHubRepo($repository); 139 + 140 + $uri = urisprintf( 141 + 'https://api.github.com/repos/%s/collaborators/%s', 142 + $github_repo, 143 + $github_user); 144 + 145 + $uri = new PhutilURI($uri); 146 + $uri->setQueryParam('access_token', $this->getAccessToken()); 147 + list($status, $body, $headers) = id(new HTTPSFuture($uri))->resolve(); 148 + 149 + // Likely status codes: 150 + // 204 No Content: Has permissions. Token might be too weak. 151 + // 404 Not Found: Not a collaborator. 152 + // 401 Unauthorized: Token is bad/revoked. 153 + 154 + $no_permission = ($status->getStatusCode() == 404); 155 + 156 + if ($no_permission) { 157 + throw new Exception( 158 + "You don't have permission to push to this repository. \n". 159 + "Push permissions for this repository are managed on GitHub."); 160 + } 161 + 162 + $scopes = BaseHTTPFuture::getHeader($headers, 'X-OAuth-Scopes'); 163 + if (strpos($scopes, 'public_repo') === false) { 164 + $provider_key = $this->provider->getProviderKey(); 165 + $refresh_token_uri = new PhutilURI("/auth/refresh/{$provider_key}/"); 166 + $refresh_token_uri->setQueryParam('scope', 'public_repo'); 167 + 168 + return id(new AphrontDialogView()) 169 + ->setUser($viewer) 170 + ->setTitle(pht('Stronger token needed')) 171 + ->appendChild(pht( 172 + 'In order to complete this action, you need a '. 173 + 'stronger GitHub token.')) 174 + ->setSubmitURI($refresh_token_uri) 175 + ->addCancelButton('/D'.$revision->getId()) 176 + ->addSubmitButton(pht('Refresh Account Link')); 177 + } 178 + } 179 + }
+1 -1
src/applications/repository/storage/PhabricatorRepository.php
··· 577 577 * @{class@libphutil:PhutilGitURI}. 578 578 * @task uri 579 579 */ 580 - private function getRemoteURIObject() { 580 + public function getRemoteURIObject() { 581 581 $raw_uri = $this->getDetail('remote-uri'); 582 582 if (!$raw_uri) { 583 583 return new PhutilURI('');