@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 Revision button for hosted git repos

Summary:
ref T182.

Simple approach of clone, patch, push. While waiting for drydock, implement a hackish mutex
setup for the workspace, which should work ok as long as there's only one committer who is
carefull about theses things.

Less obvious note: This is taking the both author and commiter's 'primary email' for the commit -
which might rub some people wrong.

Test Plan:
With a hosted repo, created some diffs and landed them.
Also clicked button for some error cases, got the right error message.

Reviewers: epriestley, #blessed_reviewers

Reviewed By: epriestley

CC: hach-que, Korvin, epriestley, aran

Maniphest Tasks: T182

Differential Revision: https://secure.phabricator.com/D7486

authored by

Aviv Eyal and committed by
epriestley
5c0edc93 ca5400d1

+413
+8
src/__phutil_library_map__.php
··· 369 369 'DifferentialFieldValidationException' => 'applications/differential/field/exception/DifferentialFieldValidationException.php', 370 370 'DifferentialFreeformFieldSpecification' => 'applications/differential/field/specification/DifferentialFreeformFieldSpecification.php', 371 371 'DifferentialFreeformFieldTestCase' => 'applications/differential/field/specification/__tests__/DifferentialFreeformFieldTestCase.php', 372 + 'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php', 372 373 'DifferentialGitSVNIDFieldSpecification' => 'applications/differential/field/specification/DifferentialGitSVNIDFieldSpecification.php', 373 374 'DifferentialHostFieldSpecification' => 'applications/differential/field/specification/DifferentialHostFieldSpecification.php', 374 375 'DifferentialHovercardEventListener' => 'applications/differential/event/DifferentialHovercardEventListener.php', ··· 383 384 'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php', 384 385 'DifferentialInlineCommentView' => 'applications/differential/view/DifferentialInlineCommentView.php', 385 386 'DifferentialJIRAIssuesFieldSpecification' => 'applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php', 387 + 'DifferentialLandingActionMenuEventListener' => 'applications/differential/landing/DifferentialLandingActionMenuEventListener.php', 388 + 'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php', 389 + 'DifferentialLandingToHostedGit' => 'applications/differential/landing/DifferentialLandingToHostedGit.php', 386 390 'DifferentialLinesFieldSpecification' => 'applications/differential/field/specification/DifferentialLinesFieldSpecification.php', 387 391 'DifferentialLintFieldSpecification' => 'applications/differential/field/specification/DifferentialLintFieldSpecification.php', 388 392 'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php', ··· 420 424 'DifferentialRevisionEditor' => 'applications/differential/editor/DifferentialRevisionEditor.php', 421 425 'DifferentialRevisionIDFieldParserTestCase' => 'applications/differential/field/specification/__tests__/DifferentialRevisionIDFieldParserTestCase.php', 422 426 'DifferentialRevisionIDFieldSpecification' => 'applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php', 427 + 'DifferentialRevisionLandController' => 'applications/differential/controller/DifferentialRevisionLandController.php', 423 428 'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php', 424 429 'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php', 425 430 'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php', ··· 2586 2591 'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery', 2587 2592 'DifferentialInlineCommentView' => 'AphrontView', 2588 2593 'DifferentialJIRAIssuesFieldSpecification' => 'DifferentialFieldSpecification', 2594 + 'DifferentialLandingActionMenuEventListener' => 'PhabricatorEventListener', 2595 + 'DifferentialLandingToHostedGit' => 'DifferentialLandingStrategy', 2589 2596 'DifferentialLinesFieldSpecification' => 'DifferentialFieldSpecification', 2590 2597 'DifferentialLintFieldSpecification' => 'DifferentialFieldSpecification', 2591 2598 'DifferentialLocalCommitsView' => 'AphrontView', ··· 2623 2630 'DifferentialRevisionEditor' => 'PhabricatorEditor', 2624 2631 'DifferentialRevisionIDFieldParserTestCase' => 'PhabricatorTestCase', 2625 2632 'DifferentialRevisionIDFieldSpecification' => 'DifferentialFieldSpecification', 2633 + 'DifferentialRevisionLandController' => 'DifferentialController', 2626 2634 'DifferentialRevisionListController' => 2627 2635 array( 2628 2636 0 => 'DifferentialController',
+39
src/applications/differential/DifferentialGetWorkingCopy.php
··· 1 + <?php 2 + 3 + /** 4 + * Can't find a good place for this, so I'm putting it in the most notably 5 + * wrong place. 6 + */ 7 + final class DifferentialGetWorkingCopy { 8 + 9 + /** 10 + * Creates and/or cleans a workspace for the requested repo. 11 + * 12 + * return ArcanistGitAPI 13 + */ 14 + public static function getCleanGitWorkspace( 15 + PhabricatorRepository $repo) { 16 + 17 + $origin_path = $repo->getLocalPath(); 18 + 19 + $path = rtrim($origin_path, '/'); 20 + $path = $path . '__workspace'; 21 + 22 + if (!Filesystem::pathExists($path)) { 23 + $repo->execxLocalCommand( 24 + 'clone -- file://%s %s', 25 + $origin_path, 26 + $path); 27 + } 28 + 29 + $workspace = new ArcanistGitAPI($path); 30 + $workspace->execxLocal('clean -f -d'); 31 + $workspace->execxLocal('checkout master'); 32 + $workspace->execxLocal('fetch'); 33 + $workspace->execxLocal('reset --hard origin/master'); 34 + $workspace->reloadWorkingCopy(); 35 + 36 + return $workspace; 37 + } 38 + 39 + }
+3
src/applications/differential/application/PhabricatorApplicationDifferential.php
··· 32 32 return array( 33 33 new DifferentialActionMenuEventListener(), 34 34 new DifferentialHovercardEventListener(), 35 + new DifferentialLandingActionMenuEventListener(), 35 36 ); 36 37 } 37 38 ··· 48 49 'changeset/' => 'DifferentialChangesetViewController', 49 50 'revision/edit/(?:(?P<id>[1-9]\d*)/)?' 50 51 => 'DifferentialRevisionEditController', 52 + 'revision/land/(?:(?P<id>[1-9]\d*))/(?P<strategy>[^/]+)/' 53 + => 'DifferentialRevisionLandController', 51 54 'comment/' => array( 52 55 'preview/(?P<id>[1-9]\d*)/' => 'DifferentialCommentPreviewController', 53 56 'save/' => 'DifferentialCommentSaveController',
+130
src/applications/differential/controller/DifferentialRevisionLandController.php
··· 1 + <?php 2 + 3 + final class DifferentialRevisionLandController extends DifferentialController { 4 + 5 + private $revisionID; 6 + private $strategyClass; 7 + private $pushStrategy; 8 + 9 + public function willProcessRequest(array $data) { 10 + $this->revisionID = $data['id']; 11 + $this->strategyClass = $data['strategy']; 12 + } 13 + 14 + public function processRequest() { 15 + $request = $this->getRequest(); 16 + $viewer = $request->getUser(); 17 + 18 + $revision_id = $this->revisionID; 19 + 20 + $revision = id(new DifferentialRevisionQuery()) 21 + ->withIDs(array($revision_id)) 22 + ->setViewer($viewer) 23 + ->executeOne(); 24 + if (!$revision) { 25 + return new Aphront404Response(); 26 + } 27 + 28 + if (is_subclass_of($this->strategyClass, 'DifferentialLandingStrategy')) { 29 + $this->pushStrategy = newv($this->strategyClass, array()); 30 + } else { 31 + throw new Exception( 32 + "Strategy type must be a valid class name and must subclass ". 33 + "DifferentialLandingStrategy. ". 34 + "'{$this->strategyClass}' is not a subclass of ". 35 + "DifferentialLandingStrategy."); 36 + } 37 + 38 + if ($request->isDialogFormPost()) { 39 + try { 40 + $this->attemptLand($revision, $request); 41 + $title = pht("Success!"); 42 + $text = pht("Revision was successfully landed."); 43 + } catch (Exception $ex) { 44 + $title = pht("Failed to land revision"); 45 + $text = 'moo'; 46 + if ($ex instanceof PhutilProxyException) { 47 + $text = hsprintf( 48 + '%s:<br><pre>%s</pre>', 49 + $ex->getMessage(), 50 + $ex->getPreviousException()->getMessage()); 51 + } else { 52 + $text = hsprintf('<pre>%s</pre>', $ex->getMessage()); 53 + } 54 + $text = id(new AphrontErrorView()) 55 + ->appendChild($text); 56 + } 57 + 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 + 65 + return id(new AphrontDialogResponse())->setDialog($dialog); 66 + } 67 + 68 + $prompt = hsprintf('%s<br><br>%s', 69 + pht( 70 + 'This will squash and rebase revision %s, and push it to '. 71 + 'origin/master.', 72 + $revision_id), 73 + pht('It is an experimental feature and may not work.')); 74 + 75 + $dialog = id(new AphrontDialogView()) 76 + ->setUser($viewer) 77 + ->setTitle(pht("Land Revision %s?", $revision_id)) 78 + ->appendChild($prompt) 79 + ->setSubmitURI($request->getRequestURI()) 80 + ->addSubmitButton(pht('Land it!')) 81 + ->addCancelButton('/D'.$revision_id); 82 + 83 + return id(new AphrontDialogResponse())->setDialog($dialog); 84 + } 85 + 86 + private function attemptLand($revision, $request) { 87 + $status = $revision->getStatus(); 88 + if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) { 89 + throw new Exception("Only Accepted revisions can be landed."); 90 + } 91 + 92 + $repository = $revision->getRepository(); 93 + 94 + if ($repository === null) { 95 + throw new Exception("revision is not attached to a repository."); 96 + } 97 + 98 + $can_push = PhabricatorPolicyFilter::hasCapability( 99 + $request->getUser(), 100 + $repository, 101 + DiffusionCapabilityPush::CAPABILITY); 102 + 103 + if (!$can_push) { 104 + throw new Exception( 105 + pht('You do not have permission to push to this repository.')); 106 + } 107 + 108 + $lock = $this->lockRepository($repository); 109 + 110 + try { 111 + $this->pushStrategy->processLandRequest( 112 + $request, 113 + $revision, 114 + $repository); 115 + } catch (Exception $e) { 116 + $lock->unlock(); 117 + throw $e; 118 + } 119 + 120 + $lock->unlock(); 121 + } 122 + 123 + private function lockRepository($repository) { 124 + $lock_name = __CLASS__.':'.($repository->getCallsign()); 125 + $lock = PhabricatorGlobalLock::newLock($lock_name); 126 + $lock->lock(); 127 + return $lock; 128 + } 129 + } 130 +
+54
src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php
··· 1 + <?php 2 + 3 + final class DifferentialLandingActionMenuEventListener 4 + extends PhabricatorEventListener { 5 + 6 + public function register() { 7 + $this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); 8 + } 9 + 10 + public function handleEvent(PhutilEvent $event) { 11 + switch ($event->getType()) { 12 + case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: 13 + $this->handleActionsEvent($event); 14 + break; 15 + } 16 + } 17 + 18 + private function handleActionsEvent(PhutilEvent $event) { 19 + $object = $event->getValue('object'); 20 + 21 + $actions = null; 22 + if ($object instanceof DifferentialRevision) { 23 + $actions = $this->renderRevisionAction($event); 24 + } 25 + 26 + $this->addActionMenuItems($event, $actions); 27 + } 28 + 29 + private function renderRevisionAction(PhutilEvent $event) { 30 + if (!$this->canUseApplication($event->getUser())) { 31 + return null; 32 + } 33 + 34 + $revision = $event->getValue('object'); 35 + 36 + $repository = $revision->getRepository(); 37 + if ($repository === null) { 38 + return null; 39 + } 40 + 41 + $strategies = id(new PhutilSymbolLoader()) 42 + ->setAncestorClass('DifferentialLandingStrategy') 43 + ->loadObjects(); 44 + foreach ($strategies as $strategy) { 45 + $actions = $strategy->createMenuItems( 46 + $event->getUser(), 47 + $revision, 48 + $repository); 49 + $this->addActionMenuItems($event, $actions); 50 + } 51 + } 52 + 53 + } 54 +
+43
src/applications/differential/landing/DifferentialLandingStrategy.php
··· 1 + <?php 2 + 3 + abstract class DifferentialLandingStrategy { 4 + 5 + public abstract function processLandRequest( 6 + AphrontRequest $request, 7 + DifferentialRevision $revision, 8 + PhabricatorRepository $repository); 9 + 10 + /** 11 + * returns PhabricatorActionView or an array of PhabricatorActionView or null. 12 + */ 13 + abstract function createMenuItems( 14 + PhabricatorUser $viewer, 15 + DifferentialRevision $revision, 16 + PhabricatorRepository $repository); 17 + 18 + /** 19 + * returns PhabricatorActionView which can be attached to the revision view. 20 + */ 21 + protected function createActionView($revision, $name, $disabled = false) { 22 + $strategy = get_class($this); 23 + $revision_id = $revision->getId(); 24 + return id(new PhabricatorActionView()) 25 + ->setRenderAsForm(true) 26 + ->setName($name) 27 + ->setHref("/differential/revision/land/{$revision_id}/{$strategy}/") 28 + ->setDisabled($disabled); 29 + } 30 + 31 + /** 32 + * might break if repository is not Git. 33 + */ 34 + protected function getGitWorkspace(PhabricatorRepository $repository) { 35 + try { 36 + return DifferentialGetWorkingCopy::getCleanGitWorkspace($repository); 37 + } catch (Exception $e) { 38 + throw new PhutilProxyException ( 39 + 'Failed to allocate a workspace', 40 + $e); 41 + } 42 + } 43 + }
+136
src/applications/differential/landing/DifferentialLandingToHostedGit.php
··· 1 + <?php 2 + 3 + final class DifferentialLandingToHostedGit 4 + extends DifferentialLandingStrategy { 5 + 6 + public function processLandRequest( 7 + AphrontRequest $request, 8 + DifferentialRevision $revision, 9 + PhabricatorRepository $repository) { 10 + 11 + $viewer = $request->getUser(); 12 + 13 + $workspace = $this->getGitWorkspace($repository); 14 + 15 + try { 16 + $this->commitRevisionToWorkspace( 17 + $revision, 18 + $workspace, 19 + $viewer); 20 + } catch (Exception $e) { 21 + throw new PhutilProxyException( 22 + 'Failed to commit patch', 23 + $e); 24 + } 25 + 26 + try { 27 + $this->pushWorkspaceRepository( 28 + $repository, 29 + $workspace, 30 + $viewer); 31 + } catch (Exception $e) { 32 + throw new PhutilProxyException( 33 + 'Failed to push changes upstream', 34 + $e); 35 + } 36 + } 37 + 38 + public function commitRevisionToWorkspace( 39 + DifferentialRevision $revision, 40 + ArcanistRepositoryAPI $workspace, 41 + PhabricatorUser $user) { 42 + 43 + $diff_id = $revision->loadActiveDiff()->getID(); 44 + 45 + $call = new ConduitCall( 46 + 'differential.getrawdiff', 47 + array( 48 + 'diffID' => $diff_id, 49 + )); 50 + 51 + $call->setUser($user); 52 + $raw_diff = $call->execute(); 53 + 54 + $missing_binary = 55 + "\nindex " 56 + . "0000000000000000000000000000000000000000.." 57 + . "0000000000000000000000000000000000000000\n"; 58 + if (strpos($raw_diff, $missing_binary) !== false) { 59 + throw new Exception("Patch is missing content for a binary file"); 60 + } 61 + 62 + $future = $workspace->execFutureLocal('apply --index -'); 63 + $future->write($raw_diff); 64 + $future->resolvex(); 65 + 66 + $workspace->reloadWorkingCopy(); 67 + 68 + $call = new ConduitCall( 69 + 'differential.getcommitmessage', 70 + array( 71 + 'revision_id' => $revision->getID(), 72 + )); 73 + 74 + $call->setUser($user); 75 + $message = $call->execute(); 76 + 77 + $author = id(new PhabricatorUser())->loadOneWhere( 78 + 'phid = %s', 79 + $revision->getAuthorPHID()); 80 + 81 + $author_string = sprintf( 82 + '%s <%s>', 83 + $author->getRealName(), 84 + $author->loadPrimaryEmailAddress()); 85 + $author_date = $revision->getDateCreated(); 86 + 87 + $workspace->execxLocal( 88 + '-c user.name=%s -c user.email=%s ' . 89 + 'commit --date=%s --author=%s '. 90 + '--message=%s', 91 + // -c will set the 'committer' 92 + $user->getRealName(), 93 + $user->loadPrimaryEmailAddress(), 94 + $author_date, 95 + $author_string, 96 + $message); 97 + } 98 + 99 + 100 + public function pushWorkspaceRepository( 101 + PhabricatorRepository $repository, 102 + ArcanistRepositoryAPI $workspace, 103 + PhabricatorUser $user) { 104 + 105 + $workspace->execxLocal("push origin HEAD:master"); 106 + } 107 + 108 + public function createMenuItems( 109 + PhabricatorUser $viewer, 110 + DifferentialRevision $revision, 111 + PhabricatorRepository $repository) { 112 + 113 + $vcs = $repository->getVersionControlSystem(); 114 + if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) { 115 + return; 116 + } 117 + 118 + if (!$repository->isHosted()) { 119 + return; 120 + } 121 + 122 + if (!$repository->isWorkingCopyBare()) { 123 + return; 124 + } 125 + 126 + $can_push = PhabricatorPolicyFilter::hasCapability( 127 + $viewer, 128 + $repository, 129 + DiffusionCapabilityPush::CAPABILITY); 130 + 131 + return $this->createActionView( 132 + $revision, 133 + pht('Land to Hosted Repository'), 134 + !$can_push); 135 + } 136 + }