Page MenuHomePhabricator

D7486.id16870.diff

D7486.id16870.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/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',
diff --git a/src/applications/differential/DifferentialGetWorkingCopy.php b/src/applications/differential/DifferentialGetWorkingCopy.php
new file mode 100755
--- /dev/null
+++ b/src/applications/differential/DifferentialGetWorkingCopy.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Can't find a good place for this, so I'm putting it in the most notably
+ * wrong place.
+ */
+final class DifferentialGetWorkingCopy {
+
+ /**
+ * Creates and/or cleans a workspace for the requested repo.
+ * tries to handle locking.
+ *
+ * return ArcanistGitAPI TODO is this the right abstraction for server side?
+ */
+ public static function getCleanGitWorkspace(
+ PhabricatorRepository $repo) {
+
+ $origin_path = $repo->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.
+ }
+}
diff --git a/src/applications/differential/application/PhabricatorApplicationDifferential.php b/src/applications/differential/application/PhabricatorApplicationDifferential.php
--- a/src/applications/differential/application/PhabricatorApplicationDifferential.php
+++ b/src/applications/differential/application/PhabricatorApplicationDifferential.php
@@ -48,6 +48,8 @@
'changeset/' => 'DifferentialChangesetViewController',
'revision/edit/(?:(?P<id>[1-9]\d*)/)?'
=> 'DifferentialRevisionEditController',
+ 'revision/land/'
+ => 'DifferentialRevisionLandController',
'comment/' => array(
'preview/(?P<id>[1-9]\d*)/' => 'DifferentialCommentPreviewController',
'save/' => 'DifferentialCommentSaveController',
diff --git a/src/applications/differential/controller/DifferentialRevisionLandController.php b/src/applications/differential/controller/DifferentialRevisionLandController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/differential/controller/DifferentialRevisionLandController.php
@@ -0,0 +1,142 @@
+<?php
+
+final class DifferentialRevisionLandController extends DifferentialController {
+
+ public function processRequest() {
+ $request = $this->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<br>Details:<br><pre>%s</pre>', $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");
+ }
+}
+
diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php
--- a/src/applications/differential/controller/DifferentialRevisionViewController.php
+++ b/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;
}

File Metadata

Mime Type
text/x-diff
Storage Engine
amazon-s3
Storage Format
Raw Data
Storage Handle
phabricator/di/l3/iebq6du42f5bervl
Default Alt Text
D7486.id16870.diff (9 KB)

Event Timeline