Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -367,6 +367,7 @@ 'DifferentialFieldValidationException' => 'applications/differential/field/exception/DifferentialFieldValidationException.php', 'DifferentialFreeformFieldSpecification' => 'applications/differential/field/specification/DifferentialFreeformFieldSpecification.php', 'DifferentialFreeformFieldTestCase' => 'applications/differential/field/specification/__tests__/DifferentialFreeformFieldTestCase.php', + 'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php', 'DifferentialGitSVNIDFieldSpecification' => 'applications/differential/field/specification/DifferentialGitSVNIDFieldSpecification.php', 'DifferentialHostFieldSpecification' => 'applications/differential/field/specification/DifferentialHostFieldSpecification.php', 'DifferentialHovercardEventListener' => 'applications/differential/event/DifferentialHovercardEventListener.php', @@ -418,6 +419,7 @@ 'DifferentialRevisionEditor' => 'applications/differential/editor/DifferentialRevisionEditor.php', 'DifferentialRevisionIDFieldParserTestCase' => 'applications/differential/field/specification/__tests__/DifferentialRevisionIDFieldParserTestCase.php', 'DifferentialRevisionIDFieldSpecification' => 'applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php', + 'DifferentialRevisionLandController' => 'applications/differential/controller/DifferentialRevisionLandController.php', 'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php', 'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php', 'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php', @@ -2625,6 +2627,7 @@ 'DifferentialRevisionEditor' => 'PhabricatorEditor', 'DifferentialRevisionIDFieldParserTestCase' => 'PhabricatorTestCase', 'DifferentialRevisionIDFieldSpecification' => 'DifferentialFieldSpecification', + 'DifferentialRevisionLandController' => 'DifferentialController', 'DifferentialRevisionListController' => array( 0 => 'DifferentialController', Index: src/applications/differential/DifferentialGetWorkingCopy.php =================================================================== --- /dev/null +++ src/applications/differential/DifferentialGetWorkingCopy.php @@ -0,0 +1,54 @@ +getLocalPath(); + + $path = rtrim($origin_path, '/'); + $path = $path . '__workspace'; + + if (! Filesystem::pathExists($path)) { + $future = new ExecFuture( + 'git clone file://%s %s', + $origin_path, + $path); + $future->resolvex(); + } + + self::obtainLock($path .'/.git/phabricator_lockfile'); + + $workspace = new ArcanistGitAPI($path); + $workspace->execxLocal('clean -fd'); + $workspace->execxLocal('checkout master'); + $workspace->execxLocal('fetch'); + $workspace->execxLocal('reset --hard origin/master'); + $workspace->reloadWorkingCopy(); + + return $workspace; + } + + private static function obtainLock($filename) { + $file = fopen($filename, 'c'); + if ($file === false) { + throw new Exception("Could not create lockfile"); + } + $locked = flock($file, LOCK_EX | LOCK_NB); + if ($locked !== true) { + throw new Exception("Could not lock lockfile"); + } + // ...and keep the lock for the remainder of run. + } +} Index: src/applications/differential/application/PhabricatorApplicationDifferential.php =================================================================== --- src/applications/differential/application/PhabricatorApplicationDifferential.php +++ src/applications/differential/application/PhabricatorApplicationDifferential.php @@ -48,6 +48,8 @@ 'changeset/' => 'DifferentialChangesetViewController', 'revision/edit/(?:(?P[1-9]\d*)/)?' => 'DifferentialRevisionEditController', + 'revision/land/' + => 'DifferentialRevisionLandController', 'comment/' => array( 'preview/(?P[1-9]\d*)/' => 'DifferentialCommentPreviewController', 'save/' => 'DifferentialCommentSaveController', Index: src/applications/differential/controller/DifferentialRevisionLandController.php =================================================================== --- /dev/null +++ src/applications/differential/controller/DifferentialRevisionLandController.php @@ -0,0 +1,142 @@ +getRequest(); + $viewer = $request->getUser(); + + $revision_id = $request->getInt('revisionID'); + + if ($revision_id == null) { + throw new Exception('"'); + } + + list($error, $details) = $this->attemptLand($revision_id, $viewer); + + if ($error === null) { + $title = "Success!"; + $text = "Revision was successfully landed."; + } else { + $title = "Failed to land revision"; + $text = hsprintf('%s
Details:
%s
', $error, $details); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle($title) + ->appendChild(phutil_tag('p', array(), $text)); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + private function attemptLand($revision_id, $viewer) { + $revision = id(new DifferentialRevisionQuery()) + ->withIDs(array($revision_id)) + ->setViewer($viewer) + ->executeOne(); + if (!$revision) { + return array("revision $revision_id not found"); + } + + $status = $revision->getStatus(); + if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) { + return array("Can only land Accepted revisions."); + } + + // TODO assert user CAN_PUSH. + + + $repo = $revision->getRepository(); + + if ($repo === null) { + return array("revision is not attached to a repository."); + } + + try { + $workspace = $this->getWorkspace($repo); + } catch (Exception $e) { + return array('Failed to allocate a workspace.', $e->getMessage()); + } + + try { + $this->commitRevisionToWorkspace($revision, $workspace, $viewer); + } catch (Exception $e) { + return array('Failed to commit patch.', $e->getMessage()); + } + + try { + $this->pushWorkspaceToHostedRepo($workspace); + } catch (Exception $e) { + return array('Failed to push changes upstream.', $e->getMessage()); + } + + return array(); + } + + function getWorkspace($repo) { + return DifferentialGetWorkingCopy::getCleanGitWorkspace($repo); + } + + function commitRevisionToWorkspace($revision, $workspace, $viewer) { + $diff = $revision->loadActiveDiff(); + $diff_id = $diff->getID(); + + $call = new ConduitCall( + 'differential.getrawdiff', + array( + 'diffID' => $diff_id, + )); + + $call->setUser($viewer); + $raw_diff = $call->execute(); + + $missing_binary = + "\nindex " + . "0000000000000000000000000000000000000000.." + . "0000000000000000000000000000000000000000\n"; + if (strpos($raw_diff, $missing_binary) !== false) { + throw new Exception("Patch is missing content for a binary file"); + } + + $tmp_file = new TempFile(); + Filesystem::writeFile($tmp_file, $raw_diff); + + $workspace->execxLocal('apply --index %s', $tmp_file); + + $workspace->reloadWorkingCopy(); + + $call = new ConduitCall( + 'differential.getcommitmessage', + array( + 'revision_id' => $revision->getID(), + )); + + $call->setUser($viewer); + $message = $call->execute(); + + $author = id(new PhabricatorUser())->loadOneWhere( + 'phid = %s', + $revision->getAuthorPHID()); + + $author_string = sprintf( + '%s <%s>', + $author->getRealName(), + $author->loadPrimaryEmailAddress()); + + $workspace->execxLocal( + '-c user.name=%s -c user.email=%s commit --message=%s --author=%s', + // -c will set the 'committer' + $viewer->getRealName(), + $viewer->loadPrimaryEmailAddress(), + $message, + $author_string); + } + + private function pushWorkspaceToHostedRepo(ArcanistGitAPI $workspace) { + // This will only work for bare, hosted repos, which I'm guessing is good enough. + $workspace->execxLocal( + "push origin HEAD:master"); + } +} + Index: src/applications/differential/controller/DifferentialRevisionViewController.php =================================================================== --- src/applications/differential/controller/DifferentialRevisionViewController.php +++ src/applications/differential/controller/DifferentialRevisionViewController.php @@ -535,6 +535,13 @@ 'href' => $request_uri->alter('download', 'true') ); + // TODO test for policy, state and repo kind. + $links[] = array( + 'icon' => 'none', + 'name' => pht('Land this revision'), + 'href' => "/differential/revision/land/?revisionID=${revision_id}" + ); + return $links; }