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