diff --git a/convert.php b/convert.php
index 5211ca261e5bcf82b88f17a4a812a269d0cb6007..cd14f5ffeb5de0597a9d88c4c437d0d1f6e8ca77 100755
--- a/convert.php
+++ b/convert.php
@@ -1,14 +1,18 @@
 #!/usr/bin/env php
 <?php
 require_once __DIR__ . '/vendor/autoload.php';
+require_once __DIR__ . '/includes/functions.php';
 
 $config = require __DIR__ . '/includes/config.php';
 $steps  = [
-    1 => __DIR__ . '/steps/convert-tickets-to-issues.php',
-//    2 => '',
+    'issues'   => __DIR__ . '/steps/convert-tickets-to-issues.php',
+    'commits'  => __DIR__ . '/steps/clone-svn-repository.php',
+    'history'  => __DIR__ . '/steps/rewrite-history.php',
+    'push'     => __DIR__ . '/steps/push-repository.php',
+    'comments' => __DIR__ . '/steps/add-comments.php',
 ];
 
-$result = [];
+$result = ['issues' => [8641 => 398]];
 foreach ($steps as $number => $step) {
     $result[$number] = require $step;
 }
diff --git a/includes/config.php.dist b/includes/config.php.dist
index be7b499ed95ba78290a76e2fdbca2fede50e5672..67ef4e445d657cbc520b8396476741ca9bd88a39 100644
--- a/includes/config.php.dist
+++ b/includes/config.php.dist
@@ -1,11 +1,17 @@
 <?php
 return [
+    // Temp paths
+    'temp-path' => '/tmp/studip-svn-git',
+
     // Trac
-    'trac-url'   => 'https://<username>:<password>@develop.studip.de/trac/login',
-    'trac-query' => 'id=8682',
+    'trac-url'      => 'https://<username>:<password>@develop.studip.de/trac/login',
+    'trac-query'    => 'version=trunk&order=id&resolution=fixed&resolution=wontfix&resolution=duplicate&resolution=worksforme',
+    'trac-revision' =>  47000, // 43791 <- 4.0 started here
+    'trac-users'    => __DIR__ . '/trac-users.txt',
 
     // GitLab
-    'gitlab-url'          => 'http://gitlabdev01.virt.uni-oldenburg.de',
+    'gitlab-api-url'      => 'http://gitlabdev01.virt.uni-oldenburg.de',
+    'gitlab-repo-url'     => 'git@gitlabdev01.virt.uni-oldenburg.de:studip/trunk.git',
     'gitlab-project'      => 'studip/trunk',
     'gitlab-access-token' => '<gitlab-token>',
     'gitlab-admin-token'  => true,
diff --git a/includes/functions.php b/includes/functions.php
new file mode 100644
index 0000000000000000000000000000000000000000..74949447c7dbf2dc19ddc36f53578d1843822eae
--- /dev/null
+++ b/includes/functions.php
@@ -0,0 +1,40 @@
+<?php
+function my_exec($command) {
+    static $level = 0;
+
+    $level += 1;
+
+    if (func_num_args() === 1 && is_array($command)) {
+        return array_map(function ($args) {
+            return call_user_func_array('my_exec', $args);
+        }, $command);
+    }
+
+    $args = func_get_args();
+    $command = array_shift($args);
+    $callback = is_callable(end($args))
+              ? array_pop($args)
+              : function ($i) { return $i; };
+
+    $command = vsprintf($command, array_slice(func_get_args(), 1));
+    exec($command, $output);
+    $result = $callback(implode("\n", $output));
+
+    $level -= 1;
+
+    return $result;
+}
+
+function removeDirectory($directory)
+{
+    $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS);
+    $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
+    foreach ($files as $file) {
+        if ($file->isDir()){
+            rmdir($file->getRealPath());
+        } else {
+            unlink($file->getRealPath());
+        }
+    }
+    rmdir($directory);
+}
diff --git a/src/GitLab.php b/src/GitLab.php
index 3069ddbdee250bc2f9fce74ff8e86b676ab6e6d9..c09ae532ddafe33b5e5f5c3229e4c4630f037f33 100644
--- a/src/GitLab.php
+++ b/src/GitLab.php
@@ -78,7 +78,7 @@ class GitLab
 		} 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, false);
+				$issue = $this->doCreateIssue($projectId, $title, $description, $createdAt, $assigneeId, $authorId, $labels, $confidential, false);
 			} else {
 				// If adding has failed for some other reason, propagate the exception back
 				throw $e;
@@ -206,5 +206,10 @@ class GitLab
 	public function getUrl() {
 		return $this->url;
 	}
+
+    public function getClient()
+    {
+        return $this->client;
+    }
 }
 ?>
diff --git a/src/Migration.php b/src/Migration.php
index 25300b926c392d8f5ab340863dcf9f582ab63e87..6d9f18068c0aff466d0404a7fd70014e881eddf7 100644
--- a/src/Migration.php
+++ b/src/Migration.php
@@ -10,8 +10,8 @@ namespace Trac2GitLab;
 class Migration
 {
 	// Communicators
-	private $gitLab;
-	private $trac;
+	public $gitLab;
+	public $trac;
 	// Configuration
 	private $addLinkToOriginalTicket;
 	private $userMapping;
@@ -92,7 +92,7 @@ class Migration
 	 * @param  array     $openTickets               Array of Trac tickets to be migrated
 	 * @param  string    $gitLabProject             GitLab project in which the issues should be created
 	 */
-	private function migrate($openTickets, $gitLabProject) {
+	public function migrate($openTickets, $gitLabProject) {
         $mapping = [];
 		foreach($openTickets as $ticket) {
 			$originalTicketId = $ticket[0];
@@ -108,7 +108,7 @@ class Migration
 			$labels = $ticket[3]['keywords'];
             $dateCreated = $ticket[3]['time']['__jsonclass__'][1];
             $dateUpdated = $ticket[3]['_ts'];
-            $confidential = $ticket[3]['sensitive'];
+            $confidential = (bool) @$ticket[3]['sensitive'];
 
             $attachments = $this->trac->getAttachments($originalTicketId);
 
@@ -162,7 +162,7 @@ class Migration
 	 * @return  string
 	 */
 	// Adapted from: https://gitlab.dyomedea.com/vdv/trac-to-gitlab/blob/master/trac2down/Trac2Down.py
-	private function translateTracToMarkdown($text) {
+	public function translateTracToMarkdown($text) {
 		$text = str_replace("\r\n", "\n", $text);
 		// Inline code block
 		$text = preg_replace('/{{{(.*?)}}}/', '`$1`', $text);
diff --git a/src/Trac.php b/src/Trac.php
index 45838ee38dbc911d9c4150379aa3b5dae91fd962..a07e4ab96e988dd689d9860c5e55961ddd3f9211 100644
--- a/src/Trac.php
+++ b/src/Trac.php
@@ -48,7 +48,6 @@ class Trac
 		$ticketIds = $this->client->execute('ticket.query', array($query));
 		foreach($ticketIds as $id) {
 			$tickets[$id] = $this->getTicket($id);
-			$tickets[$id][] = $this->getComments($id);
 		}
 		return $tickets;
 	}
@@ -111,5 +110,10 @@ class Trac
 	public function getUrl() {
 		return $this->url;
 	}
+
+    public function getClient()
+    {
+        return $this->client;
+    }
 }
 ?>
diff --git a/steps/add-comments.php b/steps/add-comments.php
new file mode 100644
index 0000000000000000000000000000000000000000..405e1d3e563bfda645eb3eaa238ba563f6791779
--- /dev/null
+++ b/steps/add-comments.php
@@ -0,0 +1,44 @@
+<?php
+use Trac2GitLab\Migration;
+
+$migration = new Migration(
+    $config['gitlab-api-url'],
+    $config['gitlab-access-token'],
+    $config['gitlab-admin-token'],
+    $config['trac-url'],
+    $config['create-trac-links'],
+    $userMapping = []
+);
+$trac   = $migration->trac;
+$gitlab = $migration->gitLab->getClient();
+
+var_dump($trac->getClient()->execute('ticket.query', ['version=trunk&max=0']));
+die;
+
+foreach ($result['issues'] as $trac_id => $gitlab_id) {
+    $comments = $trac->getComments($trac_id);
+
+    foreach ($comments as $comment) {
+        $text = $comment['text'];
+
+
+//         In [changeset:"48002"]:
+// {{{
+// #!CommitTicketReference repository="" revision="48002"
+// fixes #8641
+// }}}
+        // try {
+        //     $gitlab->api('issues')->addComment($config['gitlab-project'], $gitlab_id, [
+        //         'body'       => $comment['text'],
+        //         'sudo'       => $comment['author'],
+        //         'created_at' => $comment['time']['__jsonclass__'][1],
+        //     ]);
+        // } catch (Exception $e) {
+            $gitlab->api('issues')->addComment($config['gitlab-project'], $gitlab_id, [
+                'body'       => $text,
+                'created_at' => $comment['time']['__jsonclass__'][1],
+                'updated_at' => $comment['time']['__jsonclass__'][1],
+            ]);
+        // }
+    }
+}
diff --git a/steps/clone-svn-repository.php b/steps/clone-svn-repository.php
new file mode 100644
index 0000000000000000000000000000000000000000..3c58cee66575d6be1cc9f3756e51de25829cf6a4
--- /dev/null
+++ b/steps/clone-svn-repository.php
@@ -0,0 +1,34 @@
+<?php
+define('LOG_REGEXP', '~^r(\d+) = ([0-9a-f]+) \(refs/remotes/git-svn\)~');
+
+// Remove old git directory
+if (file_exists($config['temp-path'])) {
+    rename($config['temp-path'], $config['temp-path'] . '-' . md5(uniqid('conversion', true)));
+}
+
+// Clone svn repository as git
+$commit_mapping = [];
+
+echo "> git svn clone...";
+my_exec(
+    'git svn clone -r%u:HEAD --no-metadata --authors-file=%s svn://develop.studip.de/studip/trunk %s 2> /dev/null',
+    $config['trac-revision'],
+    $config['trac-users'],
+    $config['temp-path'],
+    function ($output) use (&$commit_mapping) {
+        $lines = explode("\n", $output);
+
+        foreach ($lines as $line) {
+            if (!preg_match(LOG_REGEXP, $line, $match)) {
+                continue;
+            }
+
+            $commit_mapping[$match[1]] = $match[2];
+        }
+
+        return $output;
+    }
+);
+echo "done\n";
+
+return $commit_mapping;
diff --git a/steps/convert-tickets-to-issues.php b/steps/convert-tickets-to-issues.php
index f0232b82aabafb748f9a3f8b7540630a38523786..23072289d74a0d055d46a9b68d4ff6144338a062 100644
--- a/steps/convert-tickets-to-issues.php
+++ b/steps/convert-tickets-to-issues.php
@@ -5,13 +5,29 @@ $issue_mapping = [];
 
 // Actually migrate
 $migration = new Migration(
-    $config['gitlab-url'],
+    $config['gitlab-api-url'],
     $config['gitlab-access-token'],
     $config['gitlab-admin-token'],
     $config['trac-url'],
     $config['create-trac-links'],
     $userMapping = []
 );
+$trac = $migration->trac->getClient();
+
+$step_size = 50;
+$page = 1;
+do {
+    $query = "{$config['trac-query']}&page={$page}&max={$step_size}";
+    try {
+        $ticket_ids = $trac->execute('ticket.query', [$query]);
+        $migration->migrate($ticket_ids, $config['gitlab-project']);
+    } catch (Exception $e) {
+        $ticket_ids = [];
+    }
+
+    $page += 1;
+} while (count($ticket_ids) > 0);
+
 return $migration->migrateQuery(
     $config['trac-query'],
     $config['gitlab-project']
diff --git a/steps/push-repository.php b/steps/push-repository.php
new file mode 100644
index 0000000000000000000000000000000000000000..34a2b99735dcc49ce90f586ffcccf57b53b6b009
--- /dev/null
+++ b/steps/push-repository.php
@@ -0,0 +1,7 @@
+<?php
+echo "> Pushing repository...";
+my_exec([
+    ['git -C %s remote add origin %s', $config['temp-path'], $config['gitlab-repo-url']],
+    ['git -C %s push origin master', $config['temp-path']]
+]);
+echo "done\n";
diff --git a/steps/rewrite-history.php b/steps/rewrite-history.php
new file mode 100644
index 0000000000000000000000000000000000000000..26723b8ccd2404feecb8dd222530fa8936b9aa05
--- /dev/null
+++ b/steps/rewrite-history.php
@@ -0,0 +1,53 @@
+<?php
+// Rewrite ticket numbers to issue numbers in commits
+echo "> rewrite commit messages...";
+$index = 0;
+// Skip latest entry for now
+foreach (array_reverse(array_slice($commit_mapping, 1)) as $sha1) {
+    $command = sprintf(
+        "git log -C %s --color=never --format=%%B -1 %s",
+        $config['temp-path'],
+        $sha1
+    );
+    $message = my_exec(
+        "git -C %s log --color=never --format=%%B -1 %s",
+        $config['temp-path'],
+        $sha1,
+        function ($message) use ($result) {
+            // Replace tickets/issues
+            $message = preg_replace_callback('/(?<=\s|^|,)#(\d+)\b/', function ($match) use ($result) {
+                if (isset($result['issues'][$match[1]])) {
+                    return '#' . $result['issues'][$match[1]];
+                }
+                return $match[0];
+            }, $message);
+
+            // Replace changesets/commits
+            $message = preg_replace_callback('/(?<=\s|^|,)\[(\d+(?:,\d+)*)\](?=\s|$|,)/', function ($match) use ($result) {
+                $changesets = explode(',', $match[1]);
+                foreach ($changesets as $index => $changeset) {
+                    if (isset($result['commits'][$changeset])) {
+                        $changesets[$index] = $result['commits'][$changeset];
+                    }
+                }
+
+                return '[' . implode(',', $changesets) . ']';
+            }, $message);
+
+            return $message;
+        }
+    );
+
+    my_exec([
+        ['git -C %s checkout -b amending HEAD~%u', $config['temp-path'], $index + 1],
+        ['git -C %s cherry-pick %s', $config['temp-path'], $sha1],
+        ['git -C %s commit --amend -m "%s"', $config['temp-path'], addslashes($message)],
+        ['git -C %s rebase --onto amending %s master', $config['temp-path'], $sha1],
+        ['git -C %s branch -d amending', $config['temp-path']],
+    ]);
+
+    unset($commit_mapping[$sha1]);
+
+    $index += 1;
+}
+echo "done\n";