diff --git a/.devcontainer/application/Dockerfile b/.devcontainer/application/Dockerfile index 8f1db9f..f9744d1 100644 --- a/.devcontainer/application/Dockerfile +++ b/.devcontainer/application/Dockerfile @@ -1,64 +1,65 @@ FROM debian:bookworm-backports # == Get ca-certificates up to date == RUN apt-get -y update RUN apt-get -y install ca-certificates # == Copy Data == COPY install_scripts /install_scripts # == Configure Ubuntu == WORKDIR /install_scripts RUN sh install_dependencies.sh RUN sh add_users.sh # == Set up the Phorge code base == RUN mkdir /srv/phorge RUN chown git:wwwgrp-phorge /srv/phorge USER git WORKDIR /srv/phorge RUN git clone https://we.phorge.it/source/phorge.git RUN git clone https://we.phorge.it/source/arcanist.git USER root WORKDIR / RUN mkdir -p /var/tmp/phd/log RUN chown phorge-daemon:2000 /var/tmp/phd/log # == Expose Ports == # Nginx EXPOSE 80 # Aphlict EXPOSE 22280 # SSH EXPOSE 2222 # == Add service config files == ADD /config/nginx.conf.org /etc/nginx/ ADD /config/fastcgi.conf /etc/nginx/ ADD /config/php-fpm.conf /etc/php/8.2/fpm/ ADD /config/php.ini /etc/php/8.2/fpm/ +ADD /config/php.ini /etc/php/8.2/cli/ ADD /config/aphlict.phorge.json /install_scripts/ # == Add Supervisord config files == RUN mkdir -p /var/log/supervisor RUN mkdir -p /etc/supervisor/conf.d/ ADD config/supervisord.conf /etc/supervisor/ COPY config/*.sv.conf /etc/supervisor/conf.d/ # == Configure Phorge SSH service == RUN mkdir /etc/phorge-ssh RUN mkdir /var/run/sshd/ RUN chmod 0755 /var/run/sshd ADD config/sshd_config.phorge /etc/phorge-ssh/ ADD config/phorge-ssh-hook.sh /etc/phorge-ssh/ RUN chown root:root /etc/phorge-ssh/* # == Copy other scripts == # COPY user-config /user-config COPY startup.sh / CMD bash ./startup.sh && supervisord diff --git a/.devcontainer/application/setup.php b/.devcontainer/application/setup.php index c1686ef..09c4f61 100644 --- a/.devcontainer/application/setup.php +++ b/.devcontainer/application/setup.php @@ -1,65 +1,68 @@ #!/usr/bin/env php setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array(1)) ->executeOne(); if ($config) { echo phutil_console_wrap("User/Password Auth Provider already configured\n"); return; } +echo phutil_console_wrap( + "Setting up dev environment with user admin/hunter2\n"); + $password1 = new PhutilOpaqueEnvelope('hunter2'); $config = id(new PhabricatorAuthProviderConfig()) ->setIsEnabled(1) ->setShouldAllowLogin(1) ->setShouldAllowRegistration(true) ->setShouldAllowLink(1) ->setShouldAllowUnlink(true) ->setProviderType('password') ->setProviderDomain('self') ->setProviderClass('PhabricatorPasswordAuthProvider') ->save(); $user = new PhabricatorUser(); $user->setUsername('admin'); $user->setRealName('admin'); $email = id(new PhabricatorUserEmail()) ->setAddress('admin@example.com') ->setIsVerified(1); $user->setIsApproved(1); id(new PhabricatorUserEditor()) ->setActor(PhabricatorUser::getOmnipotentUser()) ->createNewUser($user, $email); $xactions = array(); $xactions[] = id(new PhabricatorUserTransaction()) ->setTransactionType( PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE) ->setNewValue(true); $source = id(new PhabricatorUnknownContentSource()); $actor = PhabricatorUser::getOmnipotentUser(); $people_application_phid = id(new PhabricatorPeopleApplication()) ->getPHID(); $editor = id(new PhabricatorUserTransactionEditor()) ->setActor($actor) ->setContentSource($source) ->setActingAsPHID($people_application_phid) ->setContinueOnMissingFields(true); $editor->applyTransactions($user, $xactions); $pass = PhabricatorAuthPassword::initializeNewPassword($user, PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT) ->setPassword($password1, $user) ->save(); diff --git a/.devcontainer/application/startup.sh b/.devcontainer/application/startup.sh index fc2edc3..e45d1c8 100755 --- a/.devcontainer/application/startup.sh +++ b/.devcontainer/application/startup.sh @@ -1,59 +1,55 @@ #!/bin/bash set -x echo "Waiting for mysql" until mysql -h"$SQL_HOST" -P"$SQL_PORT" -u"$SQL_USER" -p"$SQL_PASSWORD" &> /dev/null do printf "." sleep 1 done echo -e "\nmysql ready" pushd /srv/phorge/phorge ./bin/config set mysql.host $SQL_HOST ./bin/config set mysql.port $SQL_PORT ./bin/config set mysql.user $SQL_USER ./bin/config set mysql.pass $SQL_PASSWORD ./bin/config set phabricator.base-uri $BASE_URI ./bin/config set phd.user phorge-daemon ./bin/config set diffusion.ssh-user git ./bin/config set diffusion.ssh-port 2222 ./bin/config set diffusion.allow-http-auth true ./bin/config set phabricator.developer-mode true ./bin/config set phabricator.show-prototypes true ./bin/config set darkconsole.enabled true ./bin/config set storage.mysql-engine.max-size 268435456 ./bin/config set pygments.enabled true ./bin/config set environment.append-paths '["/usr/lib/git-core"]' ./bin/config set notification.servers --stdin < /install_scripts/aphlict.phorge.json echo '["/srv/phorge/deepclone/src"]' | ./bin/config set load-libraries --stdin -php setup.php - -if [ -e /user-config/script.post ]; then - echo "Applying post-configuration script..." - /user-config/script.post -fi - popd pushd /srv/phorge/phorge/support/aphlict/server npm ci popd -find /srv/phorge/deepclone - cp /etc/nginx/nginx.conf.org /etc/nginx/nginx.conf -/srv/phorge/phorge/bin/storage upgrade --force + +pushd /srv/phorge/phorge + +./bin/storage upgrade --force + +php setup.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ab8f92f..d0edbe2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,22 +1,28 @@ 2, 'class' => array( 'DeepcloneApplication' => 'deepclone/application/DeepcloneApplication.php', + 'DeepcloneBaseTransaction' => 'xaction/DeepcloneBaseTransaction.php', 'DeepcloneController' => 'deepclone/controller/DeepcloneController.php', + 'DeepcloneSourceTransaction' => 'xaction/DeepcloneSourceTransaction.php', + 'DeepcloneTargetTransaction' => 'xaction/DeepcloneTargetTransaction.php', 'DeepcloneUIEventListener' => 'deepclone/events/DeepcloneUIEventListener.php', ), 'function' => array(), 'xmap' => array( 'DeepcloneApplication' => 'PhabricatorApplication', + 'DeepcloneBaseTransaction' => 'ManiphestTaskTransactionType', 'DeepcloneController' => 'ManiphestController', + 'DeepcloneSourceTransaction' => 'DeepcloneBaseTransaction', + 'DeepcloneTargetTransaction' => 'ManiphestTaskTransactionType', 'DeepcloneUIEventListener' => 'PhabricatorEventListener', ), )); diff --git a/src/deepclone/controller/DeepcloneController.php b/src/deepclone/controller/DeepcloneController.php index 2dc5555..6f35ce1 100644 --- a/src/deepclone/controller/DeepcloneController.php +++ b/src/deepclone/controller/DeepcloneController.php @@ -1,189 +1,236 @@ getURIData('id'); $viewer = $this->getViewer(); if (!$task_id) { throw new Exception("Task ID: $task_id"); return new Aphront404Response(); } - $task = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs(array($task_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - ) - ) - ->needSubscriberPHIDs(true) - ->executeOne(); + $task = $this->getTask($task_id); if (!$task) { throw new Exception("Task ID: $task_id"); return new Aphront404Response(); } + $transactions = $this->getTransactions($task); + if ($request->isFormPost()) { - return $this->triggerTransaction($request, $task); - // $properties = $provider->readFormValuesFromRequest($request); - // list($errors, $issues, $properties) = $provider->processEditForm( - // $request, - // $properties); - - // $xactions = array(); - - // if (!$errors) { - // if ($is_new) { - // if (!phutil_nonempty_string($config->getProviderType())) { - // $config->setProviderType($provider->getProviderType()); - // } - // if (!phutil_nonempty_string($config->getProviderDomain())) { - // $config->setProviderDomain($provider->getProviderDomain()); - // } - // } - - // $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) - // ->setTransactionType( - // PhabricatorAuthProviderConfigTransaction::TYPE_LOGIN) - // ->setNewValue($request->getInt('allowLogin', 0)); - - // $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) - // ->setTransactionType( - // PhabricatorAuthProviderConfigTransaction::TYPE_REGISTRATION) - // ->setNewValue($request->getInt('allowRegistration', 0)); - - // $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) - // ->setTransactionType( - // PhabricatorAuthProviderConfigTransaction::TYPE_LINK) - // ->setNewValue($request->getInt('allowLink', 0)); - - // $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) - // ->setTransactionType( - // PhabricatorAuthProviderConfigTransaction::TYPE_UNLINK) - // ->setNewValue($request->getInt('allowUnlink', 0)); - - // $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) - // ->setTransactionType( - // PhabricatorAuthProviderConfigTransaction::TYPE_TRUST_EMAILS) - // ->setNewValue($request->getInt('trustEmails', 0)); - - // if ($provider->supportsAutoLogin()) { - // $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) - // ->setTransactionType( - // PhabricatorAuthProviderConfigTransaction::TYPE_AUTO_LOGIN) - // ->setNewValue($request->getInt('autoLogin', 0)); - // } - - // foreach ($properties as $key => $value) { - // $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) - // ->setTransactionType( - // PhabricatorAuthProviderConfigTransaction::TYPE_PROPERTY) - // ->setMetadataValue('auth:property', $key) - // ->setNewValue($value); - // } - - // if ($is_new) { - // $config->save(); - // } - - // $editor = id(new PhabricatorAuthProviderConfigEditor()) - // ->setActor($viewer) - // ->setContentSourceFromRequest($request) - // ->setContinueOnNoEffect(true); - - // try { - // $editor->applyTransactions($config, $xactions); - // $next_uri = $config->getURI(); - - // return id(new AphrontRedirectResponse())->setURI($next_uri); - // } catch (Exception $ex) { - // $validation_exception = $ex; - // } - // } + return $this->handleClone($request, $task, $transactions); } else { return $this->buildForm($task); } } private function buildForm(ManiphestTask $task) { $form = id(new AphrontFormView()) ->setUser($this->getViewer()) - ->appendChild( - id(new AphrontFormCheckboxControl()) - ->setLabel(pht('Copy Comments')) - ->addCheckbox( - 'copy_comments', - 1, - pht('Copy all the comments of the task/s to the new task/s'), - ) - ) ->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel('Deep Clone Task') ->addCheckbox( 'deep_clone', 1, - 'Create a clone of all the sub tasks of this task', - ) - ); + pht('Create a clone of all the sub tasks of this task'))); $handle = $this->getHandle($task); $cancel_uri = "/{$handle->getName()}"; $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue(pht('Clone')) ); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($this->generateTitle($task)) - // ->setFormErrors($errors) - // ->setValidationException($validation_exception) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); $view = id(new PHUITwoColumnView()) ->setFooter(array( $form_box, )); return $this->newPage() ->setTitle($this->generateTitle($task)) ->appendChild($view); } private function generateTitle(ManiphestTask $task) { $handle = $this->getHandle($task); - return pht('Deep clone task "%s" (%s)', $task->getTitle(), $handle->getName()); + return pht('Clone task "%s" (%s)', $task->getTitle(), $handle->getName()); } private function getHandle(ManiphestTask $task) { $viewer = $this->getViewer(); $handles = $viewer->loadHandles(array($task->getPHID())); $handle = $handles[$task->getPHID()]; return $handle; } - private function triggerTransaction(AphrontRequest $request, ManiphestTask $task) { - $deepCloneRaw = $request->getStr('deep_clone'); - $deepClone = $deepCloneRaw === '1'; - $copyCommentsRaw = $request->getStr('copy_comments'); - $copyComments = $copyCommentsRaw === '1'; + private function getTask($task_id) { + $query = new ManiphestTaskQuery(); + + $task = $query + ->setViewer($this->getViewer()) + ->withPHIDs(array($task_id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->needSubscriberPHIDs(true) + ->executeOne(); + + $field_list = PhabricatorCustomField::getObjectFields( + $task, + PhabricatorCustomField::ROLE_VIEW); + $field_list + ->setViewer($this->getViewer()) + ->readFieldsFromStorage($task); + + return $task; + } + + private function getTransactions(ManiphestTask $task) { + $transactions = id(new ManiphestTransactionQuery()) + ->setViewer($this->getViewer()) + ->withObjectPHIDs(mpull([$task], 'getPHID')) + ->needComments(true) + ->execute(); + + return $transactions; + } + + private function handleClone( + AphrontRequest $request, + ManiphestTask $task, + array $transactions) { + $deep_clone = $request->getStr('deep_clone') === '1'; + + $transaction_editor = id(new ManiphestTransactionEditor()) + ->setActor($this->getViewer()) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + $task_clone = $this->cloneTask( + $task, + $transactions, + $deep_clone, + $transaction_editor); + + return id(new AphrontRedirectResponse())->setURI($task_clone->getURI()); + } + + + private function cloneTask( + ManiphestTask $task, + array $transactions, + $deep_clone, + $transaction_editor, + $parent = null) { + $task_clone = new ManiphestTask(); + + $task_clone->setTitle($task->getTitle()); + $task_clone->setDescription($task->getDescription()); + $task_clone->setAuthorPHID($task->getAuthorPHID()); + $task_clone->setOwnerPHID($task->getOwnerPHID()); + $task_clone->setStatus($task->getStatus()); + $task_clone->setPriority($task->getPriority()); + $task_clone->setSubtype($task->getSubtype()); + $task_clone->setViewPolicy($task->getViewPolicy()); + $task_clone->setEditPolicy($task->getEditPolicy()); + + $xactions = []; + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorCoreCreateTransaction::TRANSACTIONTYPE); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(DeepcloneTargetTransaction::TRANSACTIONTYPE) + ->setNewValue($task->getPHID()); + + + foreach ($transactions as &$transaction) { + if ($transaction->getTransactionType() !== 'core:customfield') { + continue; + } + + $transaction_clone = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD); + + $transaction_clone->setMetadataValue( + 'customfield:key', + $transaction->getMetadataValue('customfield:key')); + $transaction_clone->setOldValue(null); + $transaction_clone->setNewValue($transaction->getNewValue()); + + $xactions[] = $transaction_clone; + } + + if ($parent !== null) { + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTaskParentTransaction::TRANSACTIONTYPE) + ->setNewValue($parent->getPHID()); + } + + $transaction_editor->applyTransactions($task_clone, $xactions); + + $xactions = []; + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(DeepcloneSourceTransaction::TRANSACTIONTYPE) + ->setNewValue($task_clone->getPHID()); + + $transaction_editor->applyTransactions($task, $xactions); + + $task_clone->save(); + + if ($deep_clone === false) { + return $task_clone; + } + + $task_graph = id(new ManiphestTaskGraph()) + ->setViewer($this->getViewer()) + ->setSeedPHID($task->getPHID()) + ->setLimit(200) + ->loadGraph(); + + if ($task_graph->isEmpty()) { + return $task_clone; + } + + $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; + $subtask_map = $task_graph->getEdges($subtask_type); + + $subtask_list = idx($subtask_map, $task->getPHID(), array()); + + foreach ($subtask_list as &$subtask_phid) { + $sub_task = $this->getTask($subtask_phid); + $sub_transactions = $this->getTransactions($task); + + $this->cloneTask( + $sub_task, + $sub_transactions, + $deep_clone, + $transaction_editor, + $task_clone); + } - throw new Exception("Deep Clone $deepClone $deepCloneRaw Copy Comments $copyComments $copyCommentsRaw"); + return $task_clone; } } diff --git a/src/xaction/DeepcloneBaseTransaction.php b/src/xaction/DeepcloneBaseTransaction.php new file mode 100644 index 0000000..93936ac --- /dev/null +++ b/src/xaction/DeepcloneBaseTransaction.php @@ -0,0 +1,24 @@ +getNewValue(); + $target = $this->getObject(); + + return pht( + '%s cloned %s to %s', + $this->renderAuthor(), + $this->renderHandle($target->getPHID()), + $this->renderHandle($source)); + } + + public function getTitleForFeed() { + return $this->getTitle(); + } +} diff --git a/src/xaction/DeepcloneTargetTransaction.php b/src/xaction/DeepcloneTargetTransaction.php new file mode 100644 index 0000000..9dcbbe8 --- /dev/null +++ b/src/xaction/DeepcloneTargetTransaction.php @@ -0,0 +1,21 @@ +getNewValue(); + $target = $this->getObject(); + + return pht( + '%s cloned %s to %s', + $this->renderAuthor(), + $this->renderHandle($source), + $this->renderHandle($target->getPHID())); + } + + public function getTitleForFeed() { + return $this->getTitle(); + } +}