diff --git a/composer.json b/composer.json index 5d7402e48d3cb4d38df9ba8e3de09c5bfcb77b94..2c1bc9ffc5985f224dffa46dceae669826e5d65f 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 cd3ffdab9cac139be1939b0d8b46c5b938d16e48..733b43ae697d4e55eebdd06ebb7cae50e103e8ad 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 143a954389f0a8f688ce8f644998fade9bdecff8..21b10407b0da33b3a2251da0ddd062cedb4ffac4 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 9e199058bd075b54f8a0cac7ad54af4c4a8b432e..88abcebeea7f9b819db2413de1a30fc3e466f313 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 92dcb6edcc904444479eb35fc0fefc2cc4600bbe..1dd13b0dad319d03ad07111e7b56a018a9567602 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 c3c6cafd11e10c971a2bce8a1f8b2eb8c69d415e..47816d35785a88361b096b17c751f45730a686ec 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";