diff --git a/dop b/dop index 599c0ec..277df92 100755 --- a/dop +++ b/dop @@ -8,6 +8,7 @@ use DrupalPatchUtils\Command\CreateIssue; use DrupalPatchUtils\Command\Login; use DrupalPatchUtils\Command\Logout; use DrupalPatchUtils\Command\PostComment; +use DrupalPatchUtils\Command\PostIssueComment; use DrupalPatchUtils\Command\SearchIssuePatch; use DrupalPatchUtils\Command\SearchRtbcPatches; use DrupalPatchUtils\Command\ValidatePatch; @@ -23,6 +24,7 @@ $application->add(new SearchIssuePatch()); $application->add(new SearchRtbcPatches()); $application->add(new ValidatePatch()); $application->add(new ValidateRtbcPatches()); +$application->add(new PostIssueComment()); $application->add(new Login()); $application->add(new Logout()); $application->run(); diff --git a/src/DrupalPatchUtils/Command/CommandBase.php b/src/DrupalPatchUtils/Command/CommandBase.php index 0995451..b6876d9 100644 --- a/src/DrupalPatchUtils/Command/CommandBase.php +++ b/src/DrupalPatchUtils/Command/CommandBase.php @@ -10,6 +10,7 @@ namespace DrupalPatchUtils\Command; use DrupalPatchUtils\Config; +use DrupalPatchUtils\DoBrowser; use DrupalPatchUtils\Issue; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Output\OutputInterface; @@ -90,4 +91,19 @@ protected function getDialog() { protected function askConfirmation (OutputInterface $output, $question, $default = FALSE) { return $this->getDialog()->askConfirmation($output, $question, $default); } + + /** + * Ensure that the user is logged in. + * + * @param \DrupalPatchUtils\DoBrowser $browser + * The d.o. browser + * @param \Symfony\Component\Console\Output\OutputInterface $output + * The output. + */ + protected function ensureUserIsLoggedIn(DoBrowser $browser, OutputInterface $output) { + if (!$browser->loggedIn()) { + $browser->login($this->getConfig()->getDrupalUser(), $this->ask($output, "Enter your Drupal.org password: ", '', TRUE)); + } + } + } diff --git a/src/DrupalPatchUtils/Command/CreateIssue.php b/src/DrupalPatchUtils/Command/CreateIssue.php index 0b82e41..02279eb 100644 --- a/src/DrupalPatchUtils/Command/CreateIssue.php +++ b/src/DrupalPatchUtils/Command/CreateIssue.php @@ -9,6 +9,7 @@ use DrupalPatchUtils\DoBrowser; use DrupalPatchUtils\IssueSummaryTemplate; +use DrupalPatchUtils\TextEditor; use DrupalPatchUtils\Uuid; use Symfony\Component\Console\Helper\DialogHelper; use Symfony\Component\Console\Input\InputArgument; @@ -44,9 +45,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $browser = new DoBrowser(); - if (!$browser->loggedIn()) { - $browser->login($this->getConfig()->getDrupalUser(), $this->ask($output, "Enter your Drupal.org password: ", '', TRUE)); - } + $this->ensureUserIsLoggedIn($browser, $output); $project = $input->getArgument('project'); $project_form = $browser->getIssueForm($project); @@ -74,21 +73,8 @@ protected function execute(InputInterface $input, OutputInterface $output) // Allow to input the main body either via an editor or in the shell. if ($input->getOption('editor')) { - $temp_file = '/tmp/' . Uuid::generate() . ".txt"; - $filesystem = new Filesystem(); - $filesystem->touch($temp_file); - $filesystem->dumpFile($temp_file, IssueSummaryTemplate::BODY); - - $process = new Process(sprintf('vi %s', $temp_file), NULL, NULL, NULL, 3600); - - $process->setTty(TRUE); - $process->start(); - $process->wait(); - - $output->writeln($process->getOutput()); - $output->writeln($process->getErrorOutput()); - - $body_text = file_get_contents($temp_file); + $editor = new TextEditor(); + $body_text = $editor->editor($output, IssueSummaryTemplate::BODY); } else { $body_text = $dialog->ask($output, 'Enter body: ', 'TODO'); @@ -117,4 +103,5 @@ protected function execute(InputInterface $input, OutputInterface $output) return; } + } diff --git a/src/DrupalPatchUtils/Command/PostComment.php b/src/DrupalPatchUtils/Command/PostComment.php index 3d0e654..1c385e8 100644 --- a/src/DrupalPatchUtils/Command/PostComment.php +++ b/src/DrupalPatchUtils/Command/PostComment.php @@ -34,9 +34,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $issue = $this->getIssue($input->getArgument('url')); if ($issue) { $browser = new DoBrowser(); - if (!$browser->loggedIn()) { - $browser->login($this->getConfig()->getDrupalUser(), $this->ask($output, "Enter your Drupal.org password: ", '', TRUE)); - } + $this->ensureUserIsLoggedIn($browser, $output); $comment_form = $browser->getCommentForm($issue->getUri()); diff --git a/src/DrupalPatchUtils/Command/PostIssueComment.php b/src/DrupalPatchUtils/Command/PostIssueComment.php new file mode 100644 index 0000000..f5c45ed --- /dev/null +++ b/src/DrupalPatchUtils/Command/PostIssueComment.php @@ -0,0 +1,93 @@ +setName('postIssueComment') + ->setAliases(array('pic')) + ->setDescription('Posts comment on an issue to d.o.') + ->addArgument( + 'url', + InputArgument::REQUIRED, + 'What is the url/nid of the issue to retrieve?' + ) + ->addArgument( + 'files', + InputArgument::IS_ARRAY, + 'Files' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!$output instanceof ConsoleOutputInterface) { + throw new \Exception('Console output needed.'); + } + + $url = $input->getArgument('url'); + $issue = $this->getIssue($url); + + $browser = new DoBrowser(); + $this->ensureUserIsLoggedIn($browser, $output); + + $comment_form = $browser->getCommentForm($issue->getUri()); + + + $files = $input->getArgument('files'); + + $comment_editor = new CommentEditor($comment_form); + $template = $comment_editor->generateContent($issue, $files); + + $editor = new TextEditor(); + $result = $editor->editor($output, $template); + + $body = $comment_editor->getCommentText($result); + $metadata = $comment_editor->getMetadata($result); + + $comment_form->uploadFiles($files); + + $comment_form->setCommentText($body); + + if (isset($metadata['status'])) { + $comment_form->setStatus(IssueStatus::toInteger($metadata['status'])); + } + + if (isset($metadata['priority'])) { + $comment_form->setPriority(IssuePriority::toInteger($metadata['priority'])); + } + + $crawler = $browser->submitForm($comment_form->getForm()); + + if ($errors = $browser->getErrors($crawler)) { + $output->getErrorOutput()->writeln($errors); + } + else { + $uri = $browser->getClient()->getHistory()->current()->getUri(); + $output->writeln(sprintf('Posting the issue was successful: %s', $uri)); + } + + return; + } + +} diff --git a/src/DrupalPatchUtils/CommentEditor.php b/src/DrupalPatchUtils/CommentEditor.php new file mode 100644 index 0000000..c4ad0ce --- /dev/null +++ b/src/DrupalPatchUtils/CommentEditor.php @@ -0,0 +1,91 @@ +commentForm = $comment_form; + } + + public function generateContent(Issue $issue, $new_files = []) + { + $output = []; + + $output[] = "# Please enter the comment message for your changes. Lines starting"; + $output[] = "# with '#' will be ignored, and an empty message aborts the comment."; + $output[] = '#'; + $output[] = '# Comment on issue #' . $issue->getNid() . ': ' . $issue->getTitle(); + + if (!empty($new_files)) { + $output[] = '#'; + $output[] = '# Attached files'; + foreach ($new_files as $filename) { + $output[] = '# - ' . $filename; + } + } + + $output[] = '#'; + $output[] = '# Status: ' . IssueStatus::toString($this->commentForm->getStatus()); + foreach (IssueStatus::getDefinition() as $definition) { + $output[] = '# - ' . $definition['label'] . ' - ' . implode(', ', $definition['aliases']); + } + + $output[] = '#'; + $output[] = '# Priority: ' . IssuePriority::toString($this->commentForm->getPriority()); + foreach (IssuePriority::getDefinition() as $definition) { + $output[] = '# - ' . $definition['label'] . ' - ' . implode(', ', $definition['aliases']); + } + + $output[] = '#'; + $output[] = '# Tags: ' . implode(', ', $this->commentForm->getTags()); + + return implode("\n", $output); + } + + public function getCommentText($lines) + { + $array = explode("\n", $lines); + $lines = array_filter($array, function ($value) { + return !(isset($value[0]) && $value[0] == '#'); + }); + + return implode("\n", $lines); + } + + public function getMetadata($lines) + { + $metadata = array(); + foreach (explode(PHP_EOL, $lines) as $line) { + if (strpos($line, '# Tags:') === 0) { + $metadata['tags'] = trim(str_replace('# Tags: ', '', $line)); + } + if (strpos($line, '# Status:') === 0) { + $status = trim(str_replace('# Status: ', '', $line)); + + $metadata['status'] = $status; + } + if (strpos($line, '# Priority:') === 0) { + $priority = trim(str_replace('# Priority: ', '', $line)); + + $metadata['priority'] = $priority; + } + } + + return $metadata; + } + +} diff --git a/src/DrupalPatchUtils/CommentForm.php b/src/DrupalPatchUtils/CommentForm.php index fed0ddb..c701ea0 100644 --- a/src/DrupalPatchUtils/CommentForm.php +++ b/src/DrupalPatchUtils/CommentForm.php @@ -1,4 +1,5 @@ browser = $browser; + $this->form = $this->getCrawler()->selectButton('Save')->form(); + } + + public function setCommentText($text) + { + $comment = $this->form->get('nodechanges_comment_body[value]'); + $comment->setValue($text); + $this->form->set($comment); + return $this; + } + + public function uploadFiles(array $files = []) + { + $file_nr = 0; + foreach ($files as $key => $file) { + $this->form = $this->getCrawler()->selectButton('Upload')->form(); + + while (!($this->form->has("files[field_issue_files_und_$file_nr]"))) { + $file_nr++; + } + $this->form["files[field_issue_files_und_$file_nr]"]->setFilePath($file); -use Symfony\Component\DomCrawler\Form; + $this->browser->getClient()->submit($this->form); + } -class CommentForm extends DoFormBase { + $this->form = $this->getCrawler()->selectButton('Save')->form(); + } - public function setCommentText($text) { - $comment = $this->form->get('nodechanges_comment_body[value]'); - $comment->setValue($text); - $this->form->set($comment); - return $this; - } + /** + * @return null|\Symfony\Component\DomCrawler\Crawler + */ + protected function getCrawler() + { + return $this->browser->getClient()->getCrawler(); + } } diff --git a/src/DrupalPatchUtils/DoBrowser.php b/src/DrupalPatchUtils/DoBrowser.php index 30b02e0..0b18caa 100644 --- a/src/DrupalPatchUtils/DoBrowser.php +++ b/src/DrupalPatchUtils/DoBrowser.php @@ -36,14 +36,14 @@ public function __construct() { * @return bool */ public function loggedIn() { - $crawler = $this->client->request('GET', 'https://drupal.org/user/'); + $crawler = $this->client->request('GET', 'https://www.drupal.org/user/'); $log_in_button = $crawler->selectButton('Log in'); return $log_in_button->count() == 0; } public function login($user, $pass) { - $crawler = $this->client->request('GET', 'https://drupal.org/user/'); + $crawler = $this->client->request('GET', 'https://www.drupal.org/user/'); // Check if already logged in. if (($select_button = $crawler->selectButton('Log in')) && $select_button->count()) { $form = $select_button->form(); @@ -60,7 +60,7 @@ public function login($user, $pass) { public function logout() { if ($this->loggedIn()) { - $this->client->request('GET', 'https://drupal.org/user/logout'); + $this->client->request('GET', 'https://www.drupal.org/user/logout'); } } @@ -69,12 +69,12 @@ public function logout() { * @return \DrupalPatchUtils\CommentForm */ public function getCommentForm($issue_uri) { - $crawler = $this->client->request('GET', $issue_uri . '/edit'); - return new CommentForm($crawler->selectButton('Save')->form()); + $this->client->request('GET', $issue_uri . '/edit'); + return new CommentForm($this); } public function getIssueForm($project) { - $uri = 'https://drupal.org/' . 'node/add/project-issue/' . $project; + $uri = 'https://www.drupal.org/' . 'node/add/project-issue/' . $project; $crawler = $this->client->request('GET', $uri); return new IssueForm($crawler->selectButton('Save')->form()); } diff --git a/src/DrupalPatchUtils/DoFormBase.php b/src/DrupalPatchUtils/DoFormBase.php index ae2a515..b7c9ef7 100644 --- a/src/DrupalPatchUtils/DoFormBase.php +++ b/src/DrupalPatchUtils/DoFormBase.php @@ -7,6 +7,7 @@ namespace DrupalPatchUtils; +use Symfony\Component\DomCrawler\Field\ChoiceFormField; use Symfony\Component\DomCrawler\Form; class DoFormBase { @@ -60,16 +61,110 @@ public function ensureTag($value) { public function getForm () { return $this->form; } + /** * @param integer $value + * * @return $this */ - protected function setStatus($value) { + public function setStatus($value) { $status = $this->form->get('field_issue_status[und]'); $status->setValue($value); $this->form->set($status); return $this; } + public function getStatus() { + return $this->form->get('field_issue_status[und]')->getValue(); + } + + public function getPriority() { + return $this->form->get('field_issue_priority[und]')->getValue(); + } + + /** + * @param integer $value + * + * @return $this + */ + public function setPriority($value) { + $status = $this->form->get('field_issue_priority[und]'); + $status->setValue($value); + $this->form->set($status); + return $this; + } + + /** + * Returns the list of available components. + * + * @return array + */ + public function getComponents() { + /** @var ChoiceFormField $component_form */ + $component_form = $this->form->get('field_issue_component[und]'); + return $component_form->availableOptionValues(); + } + + public function setComponent($component) { + $component_form = $this->form->get('field_issue_component[und]'); + $component_form->setValue($component); + $this->form->set($component_form); + return $this; + } + + /** + * Returns the list of available versions. + * + * @return string[] + */ + public function getVersions() { + /** @var ChoiceFormField $version_form */ + $version_form = $this->form->get('field_issue_version[und]'); + return $version_form->availableOptionValues(); + } + + public function setVersion($version) { + $version_form = $this->form->get('field_issue_version[und]'); + $version_form->setValue($version); + $this->form->set($version_form); + return $this; + } + + public function setCategory($category) { + $category_form = $this->form->get('field_issue_category[und]'); + $category_form->setValue($category); + $this->form->set($category_form); + return $this; + } + + /** + * Returns the list of tags separated. + * + * @return string[] + */ + public function getTags() { + $tag_form = $this->form->get('taxonomy_vocabulary_9[und]'); + $tags = $tag_form->getValue(); + $tags = explode(', ', $tags); + + return $tags; + } + + /** + * Sets the tags of an issue. + * + * @param string[] $tags + * The tags as array. + * + * @return $this + */ + public function setTags(array $tags) { + $tag_form = $this->form->get('taxonomy_vocabulary_9[und]'); + + $tag_form->setValue(implode(', ', $tags)); + + return $this; + } + } diff --git a/src/DrupalPatchUtils/Issue.php b/src/DrupalPatchUtils/Issue.php index 3ba9070..3728c57 100644 --- a/src/DrupalPatchUtils/Issue.php +++ b/src/DrupalPatchUtils/Issue.php @@ -16,20 +16,42 @@ class Issue { */ protected $uri; + /** + * @var \Symfony\Component\DomCrawler\Crawler + */ + protected $crawler; + + protected $title; + /** * @param string $issue_id * NID or URI of an issue. */ public function __construct($issue_id) { if (is_numeric($issue_id)) { - $this->uri = 'https://drupal.org/node/' . $issue_id; + $this->uri = 'https://www.drupal.org/node/' . $issue_id; } elseif (filter_var($issue_id, FILTER_VALIDATE_URL) !== false) { $this->uri = $issue_id; } + $this->nid = str_replace('https://www.drupal.org/node/', '', $this->uri); + $this->getIssue(); } + public function getNid() { + return $this->nid; + } + + public function getTitle() { + if (!isset($this->title)) { + $this->getIssue(); + $this->title = $this->crawler->filter('h1')->first()->text(); + } + + return $this->title; + } + protected function getIssue() { $doBrowser = new DoBrowser(); // Get guzzle client. @@ -100,4 +122,4 @@ public function hasPatch() { } return FALSE; } -} \ No newline at end of file +} diff --git a/src/DrupalPatchUtils/IssueForm.php b/src/DrupalPatchUtils/IssueForm.php index eb0117a..5a42219 100644 --- a/src/DrupalPatchUtils/IssueForm.php +++ b/src/DrupalPatchUtils/IssueForm.php @@ -18,49 +18,6 @@ public function setTitle($title) { return $this; } - /** - * Returns the list of available components. - * - * @return array - */ - public function getComponents() { - /** @var ChoiceFormField $component_form */ - $component_form = $this->form->get('field_issue_component[und]'); - return $component_form->availableOptionValues(); - } - - public function setComponent($component) { - $component_form = $this->form->get('field_issue_component[und]'); - $component_form->setValue($component); - $this->form->set($component_form); - return $this; - } - - /** - * Returns the list of available versions. - * - * @return string[] - */ - public function getVersions() { - /** @var ChoiceFormField $version_form */ - $version_form = $this->form->get('field_issue_version[und]'); - return $version_form->availableOptionValues(); - } - - public function setVersion($version) { - $version_form = $this->form->get('field_issue_version[und]'); - $version_form->setValue($version); - $this->form->set($version_form); - return $this; - } - - public function setCategory($category) { - $category_form = $this->form->get('field_issue_category[und]'); - $category_form->setValue($category); - $this->form->set($category_form); - return $this; - } - public function setBody($body) { $body_form = $this->form->get('body[und][0][value]'); $body_form->setValue($body); diff --git a/src/DrupalPatchUtils/IssueMetadata.php b/src/DrupalPatchUtils/IssueMetadata.php new file mode 100644 index 0000000..833ac8a --- /dev/null +++ b/src/DrupalPatchUtils/IssueMetadata.php @@ -0,0 +1,47 @@ + $definition) { + $aliases[$definition['label']] = $status; + $aliases = array_merge($aliases, array_fill_keys($definition['aliases'], $status)); + } + + return $aliases; + } + + public static function toInteger($string) + { + $string = trim($string); + if (is_numeric($string)) { + return (string) $string; + } + $map = static::aliasMapReverse(); + + return isset($map[$string]) ? (string) $map[$string] : false; + } + + public static function toString($integer) + { + return static::getDefinition()[$integer]['label']; + } + + +} diff --git a/src/DrupalPatchUtils/IssuePriority.php b/src/DrupalPatchUtils/IssuePriority.php new file mode 100644 index 0000000..73c2d2c --- /dev/null +++ b/src/DrupalPatchUtils/IssuePriority.php @@ -0,0 +1,62 @@ + array( + 'label' => 'Critical', + 'aliases' => array( + 'critical', + 'crit', + 'c', + ), + ), + static::MAJOR => array( + 'label' => 'Major', + 'aliases' => array( + 'major', + 'maj', + 'ma', + ), + ), + static::NORMAL => array( + 'label' => 'Normal', + 'aliases' => array( + 'normal', + 'norm', + 'n', + ), + ), + static::MINOR => array( + 'label' => 'Minor', + 'aliases' => array( + 'minor', + 'min', + 'mi', + ), + ), + ); + } + +} diff --git a/src/DrupalPatchUtils/IssueStatus.php b/src/DrupalPatchUtils/IssueStatus.php new file mode 100644 index 0000000..9adde59 --- /dev/null +++ b/src/DrupalPatchUtils/IssueStatus.php @@ -0,0 +1,169 @@ + array( + 'label' => 'Active', + 'aliases' => array( + 'active', + 'a', + ), + ), + self::NEEDS_WORK => array( + 'label' => 'Needs work', + 'aliases' => array( + 'needs work', + 'nw', + 'work', + ), + ), + self::NEEDS_REVIEW => array( + 'label' => 'Needs review', + 'aliases' => array( + 'needs review', + 'nr', + 'review', + ), + ), + self::RTBC => array( + 'label' => 'Reviewed & tested by the community', + 'aliases' => array( + 'rtbc', + '+1', + ), + ), + self::PATCH_TO_BE_PORTED => array( + 'label' => 'Patch (to be ported)', + 'aliases' => array( + 'pp', + ), + ), + self::FIXED => array( + 'label' => 'Fixed', + 'aliases' => array( + 'fixed', + 'f', + ), + ), + self::POSTPONED => array( + 'label' => 'Postponed', + 'aliases' => array( + 'p', + ), + ), + self::POSTPONED_MAINTAINER_INFO => array( + 'label' => 'Postponed (maintainer needs more info)', + 'aliases' => array( + 'pmi', + ), + ), + self::CLOSED_DUPLICATE => array( + 'label' => 'Closed (duplicate)', + 'aliases' => array( + 'cd', + 'dup', + ), + ), + self::CLOSED_WONT_FIX => array( + 'label' => "Closed (won't fix)", + 'aliases' => array( + 'cwf', + ), + ), + self::CLOSED_WORKS_AS_DESIGNED => array( + 'label' => 'Closed (works as designed)', + 'aliases' => array( + 'cwad', + ), + ), + self::CLOSED_CANNOT_REPRODUCE => array( + 'label' => 'Closed (cannot reproduce)', + 'aliases' => array( + 'cnr', + ), + ), + self::CLOSED_FIXED => array( + 'label' => 'Closed (fixed)', + 'aliases' => array( + 'cf', + ), + ), + ); + } + +} diff --git a/src/DrupalPatchUtils/RtbcQueue.php b/src/DrupalPatchUtils/RtbcQueue.php index beae3af..2816c24 100644 --- a/src/DrupalPatchUtils/RtbcQueue.php +++ b/src/DrupalPatchUtils/RtbcQueue.php @@ -16,7 +16,7 @@ class RtbcQueue { - const DEFAULT_URI = 'https://drupal.org/project/issues/drupal?status=14&version=8.x&text=&priorities=All&categories=All&component=All&order=last_comment_timestamp&sort=asc'; + const DEFAULT_URI = 'https://www.drupal.org/project/issues/drupal?status=14&version=8.x&text=&priorities=All&categories=All&component=All&order=last_comment_timestamp&sort=asc'; /** * @var \Guzzle\Http\Url @@ -80,4 +80,4 @@ protected function getPage() { } return $crawler; } -} \ No newline at end of file +} diff --git a/src/DrupalPatchUtils/TextEditor.php b/src/DrupalPatchUtils/TextEditor.php new file mode 100644 index 0000000..a0d02ac --- /dev/null +++ b/src/DrupalPatchUtils/TextEditor.php @@ -0,0 +1,42 @@ +touch($temp_file); + $filesystem->dumpFile($temp_file, $text); + + $process = new Process(sprintf('vi %s', $temp_file), null, null, null, + 3600); + + $process->setTty(true); + $process->start(); + $process->wait(); + + $output->writeln($process->getOutput()); + $output->writeln($process->getErrorOutput()); + + $body_text = file_get_contents($temp_file); + return $body_text; + } + +}