From 359f28770660a5bb1b739aa6e86102ae45bc77a0 Mon Sep 17 00:00:00 2001 From: Thomas Hackl <thomas.hackl@uni-passau.de> Date: Wed, 18 Jul 2018 14:22:53 +0200 Subject: [PATCH] use transliterator, migrate milestones --- composer.json | 2 +- convert.php | 6 ++- src/GitLab.php | 49 ++++++++++++++++++-- src/Migration.php | 3 +- src/Trac.php | 18 ++++++++ steps/convert-tickets-to-issues.php | 69 +++++++++++++++++++++++------ 6 files changed, 127 insertions(+), 20 deletions(-) diff --git a/composer.json b/composer.json index 5d7402e..2c1bc9f 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ } ], "require": { - "php": ">=5.3.0", + "php": ">=5.6.0", "ext-mbstring": "*", "ext-json": "*", "m4tthumphrey/php-gitlab-api": "9.9.0", diff --git a/convert.php b/convert.php index cd3ffda..733b43a 100755 --- a/convert.php +++ b/convert.php @@ -14,7 +14,11 @@ $steps = [ 'comments' => __DIR__ . '/steps/add-comments.php', ]; -$result = ['issues' => [8641 => 398]]; +if (!class_exists('Transliterator')) { + die("PHP extension intl with Transliterator class is needed.\n"); +} + +$result = []; foreach ($steps as $number => $step) { $result[$number] = require $step; } diff --git a/src/GitLab.php b/src/GitLab.php index 143a954..21b1040 100644 --- a/src/GitLab.php +++ b/src/GitLab.php @@ -69,16 +69,21 @@ class GitLab * @param int $authorId Numeric user id of the user who created the issue. Only used in admin mode. Can be null. * @param array $labels Array of string labels to be attached to the issue. Analoguous to trac keywords. * @param bool $confidential Is this issue confidential? + * @para, int $milestoneId Optional ID of a milestone to assign this issue to * @return Gitlab\Model\Issue */ - public function createIssue($projectId, $title, $description, $createdAt, $assigneeId, $authorId, $labels, $confidential = false) { + public function createIssue($projectId, $title, $description, $createdAt, $assigneeId, $authorId, $labels, + $confidential = false, $milestoneId = 0 + ) { try { // Try to add, potentially as an admin (SUDO authorId) - $issue = $this->doCreateIssue($projectId, $title, $description, $createdAt, $assigneeId, $authorId, $labels, $confidential, $this->isAdmin); + $issue = $this->doCreateIssue($projectId, $title, $description, $createdAt, $assigneeId, $authorId, + $labels, $confidential, $milestoneId, $this->isAdmin); } catch (\Gitlab\Exception\RuntimeException $e) { // If adding has failed because of SUDO (author does not have access to the project), create an issue without SUDO (as the Admin user whose token is configured) if ($this->isAdmin) { - $issue = $this->doCreateIssue($projectId, $title, $description, $createdAt, $assigneeId, $authorId, $labels, $confidential, false); + $issue = $this->doCreateIssue($projectId, $title, $description, $createdAt, $assigneeId, $authorId, + $labels, $confidential, $milestoneId, false); } else { // If adding has failed for some other reason, propagate the exception back throw $e; @@ -170,8 +175,41 @@ class GitLab } } + /** + * Gets milestones of project. + * @param string $projectId project to check + * @param array $ids get only milestones with the given IDs. + * @param string $state if set, return only milestones with the given status + * @param string $search optional filter on milestone title or description + * @return array + */ + public function getMilestones($projectId, $ids = [], $state = '', $search = '') { + $parameters = []; + if (count($ids) > 0) { + $parameters['iids'] = $ids; + } + if ($state !== '') { + $parameters['state'] = $state; + } + if ($search !== '') { + $parameters['search'] = $search; + } + return $this->client->api('milestones')->all($projectId, $parameters); + } + + public function createMilestone($projectId, $title, $description = '', $dueDate = '', $startDate = '') { + return $this->client->api('milestones')->create($projectId, + ['title' => $title, 'description' => $description, 'due_date' => $dueDate, 'start_date' => $startDate]); + } + + public function closeMilestone($projectId, $id) { + return $this->client->api('milestones')->update($projectId, $id, ['state_event' => 'close']); + } + // Actually creates the issue - private function doCreateIssue($projectId, $title, $description, $createdAt, $assigneeId, $authorId, $labels, $confidential, $isAdmin) { + private function doCreateIssue($projectId, $title, $description, $createdAt, $assigneeId, $authorId, $labels, + $confidential, $milestoneId = 0, $isAdmin + ) { $issueProperties = array( 'title' => $title, 'description' => $description, @@ -181,6 +219,9 @@ class GitLab ); if ($confidential) { $issueProperties['confidential'] = true; + } + if ($milestoneId !== '') { + $issueProperties['milestone_id'] = $milestoneId; } if ($isAdmin) { $issueProperties['sudo'] = $authorId; diff --git a/src/Migration.php b/src/Migration.php index 9e19905..88abceb 100644 --- a/src/Migration.php +++ b/src/Migration.php @@ -156,7 +156,8 @@ class Migration // Close issue if Trac ticket was closed. if ($ticket[3]['status'] === 'closed') { $this->gitLab->closeIssue($gitLabProject, $issue['iid'], - $ticket[4][0]['time']['__jsonclass__'][1], $ticket[4][0]['author']); + isset($ticket[4]) ? $ticket[4][0]['time']['__jsonclass__'][1] : $ticket[3]['_ts'], + isset($ticket[4]) ? $ticket[4][0]['author'] : ''); } } diff --git a/src/Trac.php b/src/Trac.php index 92dcb6e..1dd13b0 100644 --- a/src/Trac.php +++ b/src/Trac.php @@ -115,5 +115,23 @@ class Trac { return $this->client; } + + /** + * Fetches all existing milestone names. + * @return array + */ + public function getMilestones() { + return $this->client->execute('ticket.milestone.getAll'); + } + + /** + * Gets the milestone with the given name. + * @param string $name + * @return array + */ + public function getMilestone($name) { + return $this->client->execute('ticket.milestone.get', [$name]); + } + } ?> diff --git a/steps/convert-tickets-to-issues.php b/steps/convert-tickets-to-issues.php index c3c6caf..47816d3 100644 --- a/steps/convert-tickets-to-issues.php +++ b/steps/convert-tickets-to-issues.php @@ -20,6 +20,11 @@ $gitlab_users = $gitlab->listUsers(); $step_size = 50; $page = 1; +// Trac Milestones that have already been created in Gitlab. +$milestones = []; +// Milestones that have already been migrated and closed. +$closedMilestones = []; + do { $query = "{$config['trac-query']}&page={$page}&max={$step_size}"; try { @@ -28,6 +33,7 @@ do { $ticket_ids = []; } + foreach ($ticket_ids as $ticket_id) { try { $ticket = $trac_client->execute('ticket.get', [$ticket_id]); @@ -44,9 +50,34 @@ do { $dateUpdated = $ticket[3]['_ts']; $confidential = (bool) @$ticket[3]['sensitive']; - $issue = $gitlab->createIssue($config['gitlab-project'], $title, + // Check if milestone must be created. + if (is_array($ticket[3]) && isset($ticket[3]['milestone']) && $ticket[3]['milestone'] !== '') { + /* + * Create a new milestone in Gitlab and use its ID it if + * it doesn't exist in Gitlab yet. + */ + if (!isset($milestones[$ticket[3]['milestone']]) || !is_array($milestones[$ticket[3]['milestone']])) { + $m = $trac->getMilestone($ticket[3]['milestone']); + $g = $gitlab->createMilestone($config['gitlab-project'], $m['name'], + translateTracToMarkdown($m['description'], $trac->getUrl()), + is_array($m['due']) ? $m['due']['__jsonclass__'][1] : '', ''); + + $milestones[$ticket[3]['milestone']] = [ + 'id' => $g['id'], + 'closed' => is_array($m['completed']) + ]; + echo "Created milestone " . $ticket[3]['milestone'] . ".\n"; + } + } + + $milestone = is_array($ticket[3]) && + $ticket[3]['milestone'] !== '' && + $milestones[$ticket[3]['milestone']] ? + $milestones[$ticket[3]['milestone']]['id'] : 0; + + $issue = $gitlab->createIssue($config['gitlab-project'], $title, $description, $dateCreated, $assigneeId, $creatorId, $labels, - $confidential); + $confidential, $milestone); echo "Created a GitLab issue #{$issue['iid']} for Trac ticket #{$ticket_id} : {$config['trac-clean-url']}/tickets/{$ticket_id}\n"; @@ -54,24 +85,25 @@ do { $attachments = $trac->getAttachments($ticket_id); + /* + * Create a transliterator for treating file names with special + * characters in them. + */ + $trans = \Transliterator::create('Latin-ASCII'); + /* * Add files attached to Trac ticket to new Gitlab issue. */ foreach ($attachments as $a) { - $a['filename'] = str_replace( - ['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü', 'ß'], - ['ae', 'oe', 'ue', 'Ae', 'Oe', 'Ue', 'ss'], - $a['filename'] - ); + // Transliterate file name, using only "safe" characters. + $filename = $trans->transliterate($a['filename']); - // TODO: Thomas! WTF! FUCK! MACHEN! SOFORT! + file_put_contents($filename, base64_decode($a['content'])); - file_put_contents($a['filename'], base64_decode($a['content'])); + $gitlab->createIssueAttachment($config['gitlab-project'], $issue['iid'], $filename, $a['author']); + unlink($filename); - $gitlab->createIssueAttachment($config['gitlab-project'], $issue['iid'], $a['filename'], $a['author']); - unlink($a['filename']); - - echo "\tAttached file " . $a['filename'] . " to issue " . $issue['iid'] . ".\n"; + echo "\tAttached file " . $filename . " to issue " . $issue['iid'] . ".\n"; } // Close issue if Trac ticket was closed. @@ -85,6 +117,17 @@ do { $gitlab->closeIssue($config['gitlab-project'], $issue['iid']); } } + + // Close milestone if necessary. + if (is_array($ticket[3]) && $ticket[3]['milestone'] !== '' && + !in_array($milestones[$ticket[3]['milestone']]['id'], $closedMilestones) + ) { + $gitlab->closeMilestone($config['gitlab-project'], $milestones[$ticket[3]['milestone']]['id']); + $closedMilestones[] = $milestones[$ticket[3]['milestone']]['id']; + + echo "\tClosed milestone " . $ticket[3]['milestone'] . ".\n"; + } + } catch (Exception $e) { throw $e; echo "Error creating issue for ticket #{$ticket_id}\n"; -- GitLab