diff --git a/app/controllers/web_migrate.php b/app/controllers/web_migrate.php
index d155ee6ec66489932bef48cf455ac43a7b488f6b..7b8def386b4edae395bc230593638e1fc7aeec6c 100644
--- a/app/controllers/web_migrate.php
+++ b/app/controllers/web_migrate.php
@@ -17,8 +17,11 @@ class WebMigrateController extends StudipController
 
         parent::before_filter($action, $args);
 
+        DBSchemaVersion::validateSchemaVersion();
+
         $this->target   = Request::int('target');
-        $this->version  = new DBSchemaVersion('studip');
+        $this->branch   = Request::get('branch', '0');
+        $this->version  = new DBSchemaVersion('studip', $this->branch);
         $this->migrator = new Migrator(
             "{$GLOBALS['STUDIP_BASE_PATH']}/db/migrations",
             $this->version,
@@ -45,9 +48,7 @@ class WebMigrateController extends StudipController
 
         $this->lock->lock(['timestamp' => time(), 'user_id' => $GLOBALS['user']->id]);
 
-        foreach (Request::getArray('versions') as $version) {
-            $this->migrator->execute($version, 'up');
-        }
+        $this->migrator->migrateTo($this->target);
 
         $this->lock->release();
 
@@ -78,51 +79,32 @@ class WebMigrateController extends StudipController
 
     public function history_action()
     {
-        $this->history = array_diff_key(
-            $this->migrator->relevantMigrations(0),
-            $this->migrator->relevantMigrations(null)
-        );
-    }
-
-    public function revert_action()
-    {
-        ob_start();
-        set_time_limit(0);
-
-        $this->lock->lock(['timestamp' => time(), 'user_id' => $GLOBALS['user']->id]);
-
-        foreach (Request::getArray('versions') as $version) {
-            $this->migrator->execute($version, 'down');
-        }
-
-        $this->lock->release();
-
-        $announcements = ob_get_clean();
-        PageLayout::postSuccess(
-            _('Die Datenbank wurde erfolgreich migriert.'),
-            array_filter(explode("\n", $announcements))
-        );
-
-        $_SESSION['migration-check'] = [
-            'timestamp' => time(),
-            'count'     => 0,
-        ];
-
-        $this->redirect('history');
+        $this->migrations = $this->migrator->relevantMigrations(0);
+        $this->offset = -1;
+        $this->target = 0;
+        $this->render_action('index');
     }
 
     public function setupSidebar($action)
     {
         $views = Sidebar::get()->addWidget(new ViewsWidget());
         $views->addLink(
-            _('Offene Migrationen'),
-            $this->url_for('index')
+            _('Migrationen ausführen'),
+            $this->url_for('index', ['branch' => $this->branch])
         )->setActive($action === 'index');
         $views->addLink(
-            _('Ausgeführte Migrationen'),
-            $this->url_for('history')
+            _('Migrationen zurücknehmen'),
+            $this->url_for('history', ['branch' => $this->branch])
         )->setActive($action === 'history');
 
+        $widget = new SelectWidget(_('Branch'), $this->url_for($action), 'branch');
+        Sidebar::get()->addWidget($widget);
+
+        foreach ($this->version->getAllBranches() as $branch) {
+            $element = new SelectElement($branch, $branch ?: 'default', $branch == $this->branch);
+            $widget->addElement($element);
+        }
+
         $widget = Sidebar::get()->addWidget(new SidebarWidget());
         $widget->setTitle(_('Aktueller Versionsstand'));
         $widget->addElement(new WidgetElement($this->version->get()));
diff --git a/app/views/web_migrate/history.php b/app/views/web_migrate/history.php
deleted file mode 100644
index ed8b853c8dd38ea6203ca372fda55351e8df9d36..0000000000000000000000000000000000000000
--- a/app/views/web_migrate/history.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<? if (STUDIP\ENV === 'development'): ?>
-<form method="post" action="<?= $controller->link_for('revert') ?>">
-    <?= CSRFProtection::tokenTag() ?>
-<? endif; ?>
-    <table class="default" id="migration-list">
-        <caption>
-        <? if (STUDIP\ENV === 'development'): ?>
-            <?= _('Die markierten Anpassungen werden beim Klick auf "Starten" zurückgesetzt:') ?>
-        <? else: ?>
-            <?= _('Diese Anpassungen wurden im System bereits ausgeführt.') ?>
-        <? endif; ?>
-        </caption>
-        <colgroup>
-        <? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
-            <col style="width: 24px">
-        <? endif; ?>
-            <col style="width: 120px">
-            <col>
-        </colgroup>
-        <thead>
-            <tr>
-            <? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
-                <th>
-                    <input type="checkbox"
-                           data-proxyfor="#migration-list tbody :checkbox"
-                           data-activates="#migration-list tfoot .button">
-                </th>
-            <? endif; ?>
-                <th><?= _('Nr.') ?></th>
-                <th><?= _('Beschreibung') ?></th>
-            </tr>
-        </thead>
-        <tbody>
-        <? foreach ($history as $number => $migration): ?>
-            <tr>
-            <? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
-                <td>
-                    <input type="checkbox"
-                           name="versions[]" value="<?= htmlReady($number) ?>">
-                </td>
-            <? endif; ?>
-                <td>
-                    <?= htmlReady($number) ?>
-                </td>
-                <td>
-                <? if ($migration->description()): ?>
-                    <?= htmlReady($migration->description()) ?>
-                <? else: ?>
-                    <em><?= _('keine Beschreibung vorhanden') ?></em>
-                <? endif ?>
-                </td>
-            </tr>
-        <? endforeach; ?>
-        </tbody>
-    <? if (STUDIP\ENV === 'development'): ?>
-        <tfoot>
-            <tr>
-                <td colspan="3">
-                <? if ($lock->isLocked($lock_data)): ?>
-                    <?= MessageBox::info(sprintf(
-                        _('Die Migration wurde %s von %s bereits angestossen und läuft noch.'),
-                        reltime($lock_data['timestamp']),
-                        htmlReady(User::find($lock_data['user_id'])->getFullName()
-                    )), [
-                        sprintf(
-                            _('Sollte während der Migration ein Fehler aufgetreten sein, so können Sie '
-                            . 'diese Sperre durch den unten stehenden Link oder das Löschen der Datei '
-                            . '<em>%s</em> auflösen.'),
-                            $lock->getFilename()
-                        )
-                    ]) ?>
-                    <?= Studip\LinkButton::create(_('Sperre aufheben'), $controller->link_for('release', $target)) ?>
-                <? else: ?>
-                    <?= Studip\Button::createAccept(_('Starten'), 'start')?>
-                <? endif; ?>
-                </td>
-            </tr>
-        </tfoot>
-    <? endif; ?>
-    </table>
-<? if (STUDIP\ENV === 'development'): ?>
-</form>
-<? endif; ?>
diff --git a/app/views/web_migrate/index.php b/app/views/web_migrate/index.php
index c9b972595b8193c70be09e90d30d608026ea752d..6bba52fbf24ed5bc499e4fdf66d0f1bd5247cecf 100644
--- a/app/views/web_migrate/index.php
+++ b/app/views/web_migrate/index.php
@@ -3,54 +3,44 @@
 <? else: ?>
 <form method="post" action="<?= $controller->link_for('migrate') ?>">
     <?= CSRFProtection::tokenTag() ?>
-<? if (isset($target)): ?>
-    <input type="hidden" name="target" value="<?= htmlReady($target) ?>">
-<? endif ?>
-<? if (STUDIP\ENV !== 'development'): ?>
-    <?= addHiddenFields('versions', array_keys($migrations)) ?>
-<? endif; ?>
-
+    <? if (isset($target)): ?>
+        <input type="hidden" name="target" value="<?= htmlReady($target) ?>">
+    <? endif ?>
+    <input type="hidden" name="branch" value="<?= htmlReady($branch) ?>">
 
     <table class="default" id="migration-list">
         <caption>
-        <? if (STUDIP\ENV === 'development'): ?>
-            <?= _('Die markierten Anpassungen werden beim Klick auf "Starten" ausgeführt:') ?>
-        <? else: ?>
             <?= _('Die hier aufgeführten Anpassungen werden beim Klick auf "Starten" ausgeführt:') ?>
-        <? endif; ?>
         </caption>
         <colgroup>
-        <? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
             <col style="width: 24px">
-        <? endif; ?>
             <col style="width: 120px">
             <col>
+            <col>
         </colgroup>
         <thead>
             <tr>
-            <? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
-                <th>
-                    <input type="checkbox"
-                           data-proxyfor="#migration-list tbody :checkbox"
-                           data-activates="#migration-list tfoot .button">
-                </th>
-            <? endif; ?>
+                <th></th>
                 <th><?= _('Nr.') ?></th>
+                <th><?= _('Name') ?></th>
                 <th><?= _('Beschreibung') ?></th>
             </tr>
         </thead>
         <tbody>
         <? foreach ($migrations as $number => $migration): ?>
+            <? $version = $migrator->migrationBranchAndVersion($number) ?>
             <tr>
-            <? if (STUDIP\ENV === 'development' && !$lock->isLocked($lock_data)): ?>
                 <td>
-                    <input type="checkbox" checked
-                           name="versions[]" value="<?= htmlReady($number) ?>">
+                    <? if ($version[0] === $branch): ?>
+                        <input type="radio" name="target" value="<?= $version[1] + $offset ?>">
+                    <? endif ?>
                 </td>
-            <? endif; ?>
                 <td>
                     <?= htmlReady($number) ?>
                 </td>
+                <td>
+                    <?= htmlReady(get_class($migration)) ?>
+                </td>
                 <td>
                 <? if ($migration->description()): ?>
                     <?= htmlReady($migration->description()) ?>
@@ -63,7 +53,7 @@
         </tbody>
         <tfoot>
             <tr>
-                <td colspan="<?= 2 + (int) (STUDIP\ENV === 'development') ?>">
+                <td colspan="4">
                 <? if ($lock->isLocked($lock_data)):
                     $user = User::find($lock_data['user_id']);
                 ?>
diff --git a/cli/migrate.php b/cli/migrate.php
index 2b43b17b82e9d4d151407d32036cbcde77cc2a57..0fe6bc35c436d6c5fe44c37c1a926d1451147ad4 100755
--- a/cli/migrate.php
+++ b/cli/migrate.php
@@ -17,14 +17,14 @@ require_once __DIR__ . '/studip_cli_env.inc.php';
 
 if (isset($_SERVER['argv'])) {
     # check for command line options
-    $options = getopt('1:d:lm:t:v');
+    $options = getopt('b:d:lm:t:v');
     if ($options === false) {
         exit(1);
     }
 
     # check for options
-    $single = false;
     $domain = 'studip';
+    $branch = '0';
     $list = false;
     $path = $STUDIP_BASE_PATH . '/db/migrations';
     $verbose = false;
@@ -32,8 +32,8 @@ if (isset($_SERVER['argv'])) {
 
     foreach ($options as $option => $value) {
         switch ($option) {
-            case '1':
-                $single = (string) $value;
+            case 'b':
+                $branch = (string) $value;
                 break;
             case 'd':
                 $domain = (string) $value;
@@ -53,7 +53,9 @@ if (isset($_SERVER['argv'])) {
         }
     }
 
-    $version = new DBSchemaVersion($domain);
+    DBSchemaVersion::validateSchemaVersion();
+
+    $version = new DBSchemaVersion($domain, $branch);
     $migrator = new Migrator($path, $version, $verbose);
 
     if ($list) {
@@ -61,15 +63,8 @@ if (isset($_SERVER['argv'])) {
 
         foreach ($migrations as $number => $migration) {
             $description = $migration->description() ?: '(no description)';
-            printf("%3d %s\n", $number, $description);
-        }
-    } elseif ($single) {
-        $direction = 'up';
-        if ($single[0] === '-') {
-            $direction = 'down';
-            $single = substr($single, 1);
+            printf("%6s %-20s %s\n", $number, get_class($migration), $description);
         }
-        $migrator->execute($single, $direction);
     } else {
         $migrator->migrateTo($target);
     }
diff --git a/cli/plugin_manager b/cli/plugin_manager
index 9a065fe5bc4c610906d6c7a5e5e226f06590fce8..e566c75a13ef419f74a5b375debda62637c29d2c 100755
--- a/cli/plugin_manager
+++ b/cli/plugin_manager
@@ -132,18 +132,22 @@ if ($args) {
 
             // show usage
             if (!$pluginname) {
-                echo 'Usage: '. $args[0] .' migrate PLUGINNAME [-l] [-t] [-v]' . "\n";
+                echo 'Usage: '. $args[0] .' migrate PLUGINNAME [-l] [-v] [-t target] [-b branch]' . "\n";
                 exit(1);
             }
 
             // parse options
-            list($errors, $options, $args) = getopts(array('l' => 'Ss l list', 'v' => 'Ss v verbose', 't'=> 'Vs t target'));
+            list($errors, $options, $args) = getopts(array('l' => 'Ss l list', 'v' => 'Ss v verbose', 't' => 'Vs t target', 'b' => 'Vs b branch'));
             $list = false;
             $verbose = false;
             $target = NULL;
+            $branch = '0';
 
             foreach ($options as $option => $value) {
                 switch ($option) {
+                    case 'b':
+                        $branch = ($value === false) ? '0' : $value;
+                        break;
                     case 'l':
                         $list = $value;
                         break;
@@ -166,7 +170,7 @@ if ($args) {
 
                     if (is_dir($plugindir . '/migrations')) {
                         // if there are migrations, migrate
-                        $schema_version = new DBSchemaVersion($plugin['name']);
+                        $schema_version = new DBSchemaVersion($plugin['name'], $branch);
                         $migrator = new Migrator($plugindir . '/migrations', $schema_version, $verbose);
 
                         if ($list) {
@@ -175,7 +179,7 @@ if ($args) {
                             foreach ($migrations as $number => $migration) {
                                 $description = $migration->description() ?: '(no description)';
 
-                                printf("%3d %-20s %s\n", $number, get_class($migration), $description);
+                                printf("%6s %-20s %s\n", $number, get_class($migration), $description);
                             }
                         } else {
                             $migrator->migrateTo($target);
@@ -214,7 +218,7 @@ if ($args) {
                     if (is_dir($plugindir . '/migrations')) {
                         $schema_version = new DBSchemaVersion($plugin['name']);
                         $migrator = new Migrator($plugindir . '/migrations', $schema_version);
-                        $migrator->migrate_to(0);
+                        $migrator->migrateTo(0);
                     }
 
                     echo 'Das Plugin '. $plugin['name'] .' wurde ausgetragen.' . "\n";
diff --git a/db/migrations/03_step_87_extern_configurations.php b/db/migrations/03_step_87_extern_configurations.php
index e4cf33bf553c55ca99f115d3a699bfa42a38d628..5b7b1f9d5b4d5155db2c4074601cbf4ffd7df616 100644
--- a/db/migrations/03_step_87_extern_configurations.php
+++ b/db/migrations/03_step_87_extern_configurations.php
@@ -1,18 +1,4 @@
 <?php
-$requirements = [
-    "lib/functions.php",
-    $GLOBALS["RELATIVE_PATH_EXTERN"]."/extern_config.inc.php",
-    $GLOBALS["RELATIVE_PATH_EXTERN"]."/lib/ExternConfig.class.php",
-    $GLOBALS["RELATIVE_PATH_EXTERN"]."/lib/ExternConfigIni.class.php",
-    $GLOBALS["RELATIVE_PATH_EXTERN"]."/lib/ExternConfigDb.class.php",
-];
-
-foreach ($requirements as $file) {
-    if (!file_exists($file)) {
-        throw new Exception('Migration is not compatible with your Stud.IP version');
-    }
-    require_once $file;
-}
 
 class Step87ExternConfigurations extends Migration
 {
diff --git a/db/migrations/18_step_00139_upload_file_reorg.php b/db/migrations/18_step_00139_upload_file_reorg.php
index cb301540d6555b0f5cf5c74999368f61319ba724..51201e2a54e9f686ed775fd39780e640ff7c1327 100644
--- a/db/migrations/18_step_00139_upload_file_reorg.php
+++ b/db/migrations/18_step_00139_upload_file_reorg.php
@@ -1,14 +1,4 @@
 <?php
-$requirements = [
-    __DIR__ . '/lib/datei.inc.php',
-];
-
-foreach ($requirements as $file) {
-    if (!file_exists($file)) {
-        throw new Exception('Migration is not compatible with your Stud.IP version');
-    }
-    require_once $file;
-}
 
 class Step00139UploadFileReorg extends Migration
 {
diff --git a/db/migrations/202011031_add_missing_indices_resources.php b/db/migrations/202011031_add_missing_indices_resources.php
index 063c60975e75e71cc46b5c8a878fc0384fca8012..a3da2c2bb26995e38406147c37103effd6ae19a5 100644
--- a/db/migrations/202011031_add_missing_indices_resources.php
+++ b/db/migrations/202011031_add_missing_indices_resources.php
@@ -12,6 +12,14 @@ class AddMissingIndicesResources extends Migration
 
     public function up()
     {
+        // avoid running this migration twice
+        $query = "SHOW INDEX FROM resource_temporary_permissions WHERE Key_name = 'user_id'";
+        $result = DBManager::get()->query($query);
+
+        if ($result && $result->rowCount() > 0) {
+            return;
+        }
+
         $query = "ALTER TABLE `resource_temporary_permissions` ADD INDEX (`user_id`)";
         DBManager::get()->exec($query);
 
diff --git a/db/migrations/20201103_add_missing_indices.php b/db/migrations/20201103_add_missing_indices.php
index 70124c919fac7745f09f5108f71080586557c784..b1b652bc53de0df53052f10e199202dd62dcbd31 100644
--- a/db/migrations/20201103_add_missing_indices.php
+++ b/db/migrations/20201103_add_missing_indices.php
@@ -13,6 +13,14 @@ class AddMissingIndices extends Migration
 
     public function up()
     {
+        // avoid running this migration twice
+        $query = "SHOW INDEX FROM activities WHERE Key_name = 'object_id'";
+        $result = DBManager::get()->query($query);
+
+        if ($result && $result->rowCount() > 0) {
+            return;
+        }
+
         $query = "ALTER TABLE `activities`
                   ADD INDEX `object_id` (`object_id`(32))";
         DBManager::get()->exec($query);
diff --git a/db/migrations/20201114_migration_history.php b/db/migrations/20201114_migration_history.php
deleted file mode 100644
index 7d82a8c39cb932e014c852a5f4f4d37716f4bdb5..0000000000000000000000000000000000000000
--- a/db/migrations/20201114_migration_history.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-class MigrationHistory extends Migration
-{
-    public function description()
-    {
-        return 'Extends migration table to allow a history of who did what when';
-    }
-
-    public function up()
-    {
-        $query = "ALTER TABLE `schema_versions`
-                  ADD COLUMN `user_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NULL DEFAULT NULL,
-                  ADD COLUMN `mkdate` INT(11) UNSIGNED NULL DEFAULT NULL";
-        DBManager::get()->exec($query);
-    }
-
-    public function down()
-    {
-        $query = "ALTER TABLE `schema_versions`
-                  DROP COLUMN `user_id`,
-                  DROP COLUMN `mkdate`";
-        DBManager::get()->exec($query);
-    }
-}
diff --git a/db/migrations/20210317_change_blubber_thread_following.php b/db/migrations/20210317_change_blubber_thread_following.php
index 96098cdeb23868bc218f517d6c90414f993dd3ca..dadd953ec43d765b99808651a12fff9b458ed65d 100644
--- a/db/migrations/20210317_change_blubber_thread_following.php
+++ b/db/migrations/20210317_change_blubber_thread_following.php
@@ -8,6 +8,14 @@ class ChangeBlubberThreadFollowing extends Migration
 
     public function up()
     {
+        // avoid running this migration twice
+        $query = "SHOW TABLES LIKE 'blubber_threads_followstates'";
+        $result = DBManager::get()->query($query);
+
+        if ($result && $result->rowCount() > 0) {
+            return;
+        }
+
         // Alter table to reflect new state
         $query = "RENAME TABLE `blubber_threads_unfollow` TO `blubber_threads_followstates`";
         DBManager::get()->exec($query);
diff --git a/db/migrations/20210322_migration_history_reworked.php b/db/migrations/20210322_migration_history_reworked.php
index 4863e7276fc58ccb1447d886b92c95148099ebbd..4809b8de608438814f1dbaffb051e81639478817 100644
--- a/db/migrations/20210322_migration_history_reworked.php
+++ b/db/migrations/20210322_migration_history_reworked.php
@@ -3,17 +3,11 @@ class MigrationHistoryReworked extends Migration
 {
     public function description()
     {
-        return 'Reverts migration 20201114 and adds appropriate log actions';
+        return 'Add log actions for migrations';
     }
 
     public function up()
     {
-        // Drop columns
-        $query = "ALTER TABLE `schema_versions`
-                  DROP COLUMN `user_id`,
-                  DROP COLUMN `mkdate`";
-        DBManager::get()->exec($query);
-
         // Add log actions
         $query = "INSERT IGNORE INTO log_actions (
                     `action_id`, `name`, `description`, `info_template`, `active`, `expires`, `mkdate`, `chdate`
@@ -46,11 +40,5 @@ class MigrationHistoryReworked extends Migration
         $statement = DBManager::get()->prepare($query);
         $statement->execute([':name' => 'MIGRATE_UP']);
         $statement->execute([':name' => 'MIGRATE_DOWN']);
-
-        // Add columns
-        $query = "ALTER TABLE `schema_versions`
-                  ADD COLUMN `user_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NULL DEFAULT NULL,
-                  ADD COLUMN `mkdate` INT(11) UNSIGNED NULL DEFAULT NULL";
-        DBManager::get()->exec($query);
     }
 }
diff --git a/db/migrations/259_migrations_reloaded.php b/db/migrations/259_migrations_reloaded.php
index 722c7e243918d680072a297578e303153406af25..b73de5c327922d7003c10269e8f884f31b29fcec 100644
--- a/db/migrations/259_migrations_reloaded.php
+++ b/db/migrations/259_migrations_reloaded.php
@@ -1,58 +1,35 @@
 <?php
+
 class MigrationsReloaded extends Migration
 {
     public function description()
     {
-        return 'switch from a single successive migration version number to '
-             . 'a collection of already executed migrations';
+        return 'add branch column to schema_version';
     }
 
     public function up()
     {
-        $query = "CREATE TABLE IF NOT EXISTS `schema_versions` (
-                    `domain` VARCHAR(255) COLLATE latin1_bin NOT NULL,
-                    `version` BIGINT(20) UNSIGNED NOT NULL,
-                    PRIMARY KEY `domain` (`domain`, `version`)
-                ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC";
-        DBManager::get()->exec($query);
-
-        $query = "SELECT `domain`, `version`
-                  FROM `schema_version`
-                  WHERE `version` > 0";
-        $rows = DBManager::get()->query($query)->fetchAll(PDO::FETCH_NUM);
-
-        $query = "INSERT INTO `schema_versions`
-                  VALUES (:domain, :version)";
-        $statement = DBManager::get()->prepare($query);
-        foreach ($rows as list($domain, $version)) {
-            $statement->bindValue(':domain', $domain);
-
-            for ($i = 1; $i <= $version; $i += 1) {
-                $statement->bindValue(':version', $i);
-                $statement->execute();
-            }
-        }
-
-       $query = "DROP TABLE IF EXISTS `schema_version`";
-       DBManager::get()->exec($query);
+        $db = DBManager::get();
+
+        $sql = "ALTER TABLE schema_version
+                CHANGE domain domain VARCHAR(255) COLLATE latin1_bin NOT NULL,
+                ADD branch VARCHAR(64) COLLATE latin1_bin NOT NULL DEFAULT '0' AFTER domain,
+                DROP PRIMARY KEY,
+                ADD PRIMARY KEY (domain, branch)";
+        $db->exec($sql);
     }
 
     public function down()
     {
-        $query = "CREATE TABLE IF NOT EXISTS `schema_version` (
-                    `domain` VARCHAR(255) COLLATE latin1_bin NOT NULL,
-                    `version` INT(11) UNSIGNED NOT NULL,
-                    PRIMARY KEY (`domain`)
-                  ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC";
-        DBManager::get()->exec($query);
+        $db = DBManager::get();
 
-        $query = "INSERT IGNORE INTO `schema_version`
-                  SELECT `domain`, MAX(`version`)
-                  FROM `schema_versions`
-                  GROUP BY `domain`";
-        DBManager::get()->exec($query);
+        $sql = "DELETE FROM schema_version WHERE branch != '0'";
+        $db->exec($sql);
 
-        $query = "DROP TABLE IF EXISTS `schema_versions`";
-        DBManager::get()->exec($query);
+        $sql = 'ALTER TABLE schema_version
+                DROP PRIMARY KEY,
+                ADD PRIMARY KEY (domain),
+                DROP branch';
+        $db->exec($sql);
     }
-};
+}
diff --git a/lib/classes/PluginAdministration.php b/lib/classes/PluginAdministration.php
index 8a3dda5bc5e1155192323aca2fd270b8dbe7162c..f42f594794e2e667db8dff8a612de89946a5d0e4 100644
--- a/lib/classes/PluginAdministration.php
+++ b/lib/classes/PluginAdministration.php
@@ -251,7 +251,8 @@ class PluginAdministration
 
             if (is_dir($new_pluginpath . '/migrations')) {
                 $migrator = new Migrator($new_pluginpath . '/migrations', $schema_version);
-                $new_version = $migrator->topVersion();
+                $all_branches = array_fill_keys($schema_version->getAllBranches(), 0);
+                $new_version = $migrator->topVersion(true) + $all_branches;
             }
 
             $migrator = new Migrator($plugindir . '/migrations', $schema_version);
diff --git a/lib/classes/StudipCacheFactory.class.php b/lib/classes/StudipCacheFactory.class.php
index 805aa9d8be8ae3c41bda9849f89766ad2050fbb0..cc7be15dd434a5adfc6c731f6b5d36f5b4da77b0 100644
--- a/lib/classes/StudipCacheFactory.class.php
+++ b/lib/classes/StudipCacheFactory.class.php
@@ -189,7 +189,7 @@ class StudipCacheFactory
         # default class
         if (is_null($cache_class)) {
             $version = new DBSchemaVersion();
-            if (!$version->contains(224)) {
+            if ($version->get() < 224) {
                 // db cache is not yet available, use StudipMemoryCache
                 return 'StudipMemoryCache';
             }
diff --git a/lib/migrations/DBSchemaVersion.php b/lib/migrations/DBSchemaVersion.php
index 797a1c8b5e451519c75c9aa289471abc57d53b79..207a804aee7744322c3192440cce51d3141befac 100644
--- a/lib/migrations/DBSchemaVersion.php
+++ b/lib/migrations/DBSchemaVersion.php
@@ -6,8 +6,8 @@
  *
  * @author    Elmar Ludwig
  * @copyright 2007 Elmar Ludwig
- * @license   GPL2 or any later version
- * @package   migrations
+ * @license    GPL2 or any later version
+ * @package migrations
  */
 class DBSchemaVersion implements SchemaVersion
 {
@@ -19,26 +19,32 @@ class DBSchemaVersion implements SchemaVersion
     private $domain;
 
     /**
-     * schema versions
+     * branch of schema version
      *
-     * @var array
+     * @var string
      */
-    private $versions = [];
+    private $branch;
 
     /**
+     * current schema version numbers
+     *
+     * @access private
      * @var array
      */
-    private $data = [];
+    private $versions;
 
     /**
      * Initialize a new DBSchemaVersion for a given domain.
      * The default domain name is 'studip'.
      *
      * @param string $domain domain name (optional)
+     * @param string $branch schema branch (optional)
      */
-    public function __construct($domain = 'studip')
+    public function __construct($domain = 'studip', $branch = 0)
     {
         $this->domain = $domain;
+        $this->branch = $branch;
+        $this->versions = [0];
         $this->initSchemaInfo();
     }
 
@@ -53,132 +59,134 @@ class DBSchemaVersion implements SchemaVersion
     }
 
     /**
-     * Initialize the current schema version.
+     * Retrieve the branch of this schema.
+     *
+     * @return string schema branch
      */
-    private function initSchemaInfo()
+    public function getBranch()
     {
-        $this->data = [];
-
-        try {
-            $query = "SELECT version FROM schema_versions WHERE domain = ?";
-            $statement = DBManager::get()->prepare($query);
-            $statement->execute([$this->domain]);
-            $this->versions = $statement->fetchAll(PDO::FETCH_COLUMN);
-        } catch (PDOException $e) {
-            $query = "SELECT version FROM schema_version WHERE domain = ?";
-            $statement = DBManager::get()->prepare($query);
-            $statement->execute([$this->domain]);
-            $this->versions = range(1, $statement->fetchColumn());
-        }
+        return $this->branch;
     }
 
     /**
-     * Retrieve the current schema version.
+     * Retrieve all branches of this schema.
      *
-     * @return int schema version
+     * @return array all schema branches
      */
-    public function get()
+    public function getAllBranches()
     {
-        return $this->versions ? max($this->versions) : 0;
+        return array_keys($this->versions);
     }
 
     /**
-     * Returns whether the given version is already present for the given
-     * domain.
-     *
-     * @param  int $version Version number
-     * @return bool
+     * Check whether the current schema_version supports branches.
      */
-    public function contains($version)
+    private function branchSupported()
     {
-        return in_array($version, $this->versions);
+        $result = DBManager::get()->query("DESCRIBE schema_version 'branch'");
+        return $result && $result->rowCount() > 0;
     }
 
     /**
-     * Set the current schema version.
-     *
-     * @param int $version new schema version
+     * Initialize the current schema versions.
      */
-    public function add($version)
+    private function initSchemaInfo()
     {
-        $version = (int) $version;
-
-        try {
-            $query = "INSERT INTO `schema_versions` (`domain`, `version`)
-                      VALUES (?, ?)";
-            DBManager::get()->execute($query, [
-                $this->domain,
-                $version,
-            ]);
+        if (!$this->branchSupported()) {
+            $query = "SELECT 0, version FROM schema_version WHERE domain = ?";
+        } else {
+            $query = "SELECT branch, version FROM schema_version WHERE domain = ? ORDER BY branch";
+        }
+        $statement = DBManager::get()->prepare($query);
+        $statement->execute([$this->domain]);
+        $versions = $statement->fetchAll(PDO::FETCH_COLUMN | PDO::FETCH_GROUP | PDO::FETCH_UNIQUE);
 
-            StudipLog::log(
-                'MIGRATE_UP',
-                $version,
-                $this->domain
-            );
-        } catch (PDOException $e) {
-            $query = "UPDATE `schema_version`
-                      SET `version` = ?
-                      WHERE `domain` = ?";;
-            DBManager::get()->execute($query, [
-                $version,
-                $this->domain,
-            ]);
+        if ($versions) {
+            $this->versions = array_map('intval', $versions);
         }
-        NotificationCenter::postNotification(
-            'SchemaVersionDidUpdate',
-            $this->domain,
-            $version
-        );
     }
 
     /**
-     * Removes a schema version.
+     * Retrieve the current schema version.
+     *
+     * @param string $branch schema branch (optional)
+     * @return int schema version
+     */
+    public function get($branch = 0)
+    {
+        return $this->versions[$branch ?: $this->branch];
+    }
+
+    /**
+     * Set the current schema version.
      *
-     * @param int $version schema version to remove
+     * @param int $version new schema version
+     * @param string $branch schema branch (optional)
      */
-    public function remove($version)
+    public function set($version, $branch = 0)
     {
-        $version = (int) $version;
+        $this->versions[$branch ?: $this->branch] = (int) $version;
 
-        try {
-            $query = "DELETE FROM `schema_versions`
-                      WHERE `domain` = ? AND `version` = ?";
-            DBManager::get()->execute($query, [
+        if (!$this->branchSupported()) {
+            $query = "INSERT INTO schema_version (domain, version)
+                      VALUES (?, ?)
+                      ON DUPLICATE KEY UPDATE version = VALUES(version)";
+            $statement = DBManager::get()->prepare($query);
+            $statement->execute([
                 $this->domain,
                 $version
             ]);
-
-            StudipLog::log(
-                'MIGRATE_DOWN',
-                $version,
-                $this->domain
-            );
-        } catch (PDOException $e) {
-            $query = "UPDATE `schema_version`
-                      SET `version` = ?
-                      WHERE `domain` = ?";
-            DBManager::get()->execute($query, [
-                $version,
+        } else {
+            $query = "INSERT INTO schema_version (domain, branch, version)
+                      VALUES (?, ?, ?)
+                      ON DUPLICATE KEY UPDATE version = VALUES(version)";
+            $statement = DBManager::get()->prepare($query);
+            $statement->execute([
                 $this->domain,
+                $branch ?: $this->branch,
+                $version
             ]);
         }
         NotificationCenter::postNotification(
-            'SchemaVersionDidDelete',
+            'SchemaVersionDidUpdate',
             $this->domain,
             $version
         );
     }
 
     /**
-     * @param $domain
-     * @param $version
-     * @return string
+     * Validate correct structure of schema_version table.
      */
-    static public function exists($domain, $version)
+    public static function validateSchemaVersion()
     {
-        return (bool)DBManager::get()->fetchColumn(
-            "SELECT 1 FROM schema_versions WHERE `domain` = ? AND `version` = ?",
-            [$domain, $version]);
+        $db = DBManager::get();
+        $result = $db->query("SHOW TABLES LIKE 'schema_versions'");
+
+        if ($result && $result->rowCount() > 0) {
+            $backported_migrations = [
+                20200306, 20200306, 20200713, 20200811, 20200909,
+                20200910, 20201002, 20201103, 202011031, 20210317
+            ];
+
+            $query = "DELETE FROM schema_versions
+                      WHERE domain = 'studip' AND version in (?)";
+            $db->execute($query, [$backported_migrations]);
+
+            $query = "CREATE TABLE schema_version (
+                        domain VARCHAR(255) COLLATE latin1_bin NOT NULL,
+                        branch VARCHAR(64) COLLATE latin1_bin NOT NULL DEFAULT '0',
+                        version INT(11) UNSIGNED NOT NULL,
+                        PRIMARY KEY (domain, branch)
+                      ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC";
+            $db->exec($query);
+
+            $query = "INSERT INTO schema_version
+                      SELECT domain, '0', MAX(version) FROM schema_versions
+                      GROUP BY domain";
+            $db->exec($query);
+
+            $query = "DROP TABLE schema_versions";
+            $db->exec($query);
+        }
     }
 }
diff --git a/lib/migrations/Migration.php b/lib/migrations/Migration.php
index 355bfe2dee502d2acfe32145468886f72721ccf6..d989d5341e36eceec6cd1d406f302d15419a3662 100644
--- a/lib/migrations/Migration.php
+++ b/lib/migrations/Migration.php
@@ -100,12 +100,28 @@ abstract class Migration
      *
      * @param string $format,... printf-style format string and parameters
      */
-    protected function announce($format /* , ... */)
+    public function announce($format /* , ... */)
     {
         # format message
         $args = func_get_args();
         $message = vsprintf(array_shift($args), $args);
 
-        return $this->write(Migrator::mark($message));
+        return $this->write($this->mark($message));
     }
+
+    /**
+     * Pads and highlights a given text to a specific length with the given
+     * sign.
+     *
+     * @param string $text
+     * @param string $sign
+     */
+    protected function mark($text, $sign = '=')
+    {
+        $text = trim($text);
+        if ($text) {
+            $text = " {$text} ";
+        }
+        return str_pad("{$sign}{$sign}{$text}", 79, $sign, STR_PAD_RIGHT);
+     }
 }
diff --git a/lib/migrations/Migrator.php b/lib/migrations/Migrator.php
index 92ec808c8dee567284eb18ba7390c3b5c5aabc91..eb0193a08dabec4177f67b6013b03d94e399eab3 100644
--- a/lib/migrations/Migrator.php
+++ b/lib/migrations/Migrator.php
@@ -38,16 +38,14 @@
  *
  * (\d+)_([a-z_]+).php   // (index)_(name).php
  *
- * 20180524110400_my_first_migration.php
- * 20180812152300_another_migration.php
- * 20181110100900_and_one_last.php
+ * 001_my_first_migration.php
+ * 002_another_migration.php
+ * 003_and_one_last.php
  *
- * Those numbers are used to order your migrations. Use the current time to
- * define the chronological order of migrations. Gaps are allowed. In previous
- * versions of the migration system, the numbers were naturally ordered starting
- * with 1 but that proved to be rather unflexible regarding bug fixes that
- * needed a migration to be executed. Thus, every executed migration number is
- * stored and you may add a migration lateron between two other migrations.
+ * Those numbers are used to order your migrations. The first migration has
+ * to be a 1 (but you can use leading 0). Every following migration has to be
+ * the successor to the previous migration. No gaps are allowed. Just use
+ * natural numbers starting with 1.
  *
  * When migrating those numbers are used to determine the migrations needed to
  * fulfill the target version.
@@ -102,7 +100,7 @@
  *   $migrator = new Migrator($path, $version, $verbose);
  *
  *   # now migrate to target version
- *   $migrator->migrateTo(20181128100139);
+ *   $migrator->migrateTo(5);
  *
  * If you want to migrate to the highest migration, you can just use NULL as
  * parameter:
@@ -116,8 +114,6 @@
  */
 class Migrator
 {
-    const FILE_REGEXP = '/\b(\d+)([_-][_a-z0-9]+)+\.php$/';
-
     /**
      * Direction of migration, either "up" or "down"
      *
@@ -135,9 +131,9 @@ class Migrator
     /**
      * Specifies the target version, may be NULL (alias for "highest migration")
      *
-     * @var int
+     * @var array
      */
-    private $target_version;
+    private $target_versions;
 
     /**
      * How verbose shall the migrator be?
@@ -192,8 +188,8 @@ class Migrator
      * the current schema version (provided by the SchemaVersion object) and a
      * target version calling the methods #up and #down in sequence.
      *
-     * @param mixed  the target version as an integer or NULL thus migrating to
-     *               the top migration
+     * @param mixed  the target version as an integer, array or NULL thus
+     *               migrating to the top migrations
      */
     public function migrateTo($target_version)
     {
@@ -201,83 +197,87 @@ class Migrator
 
         # you're on the right version
         if (empty($migrations)) {
-            $this->log("You are already at %d.\n", $this->schema_version->get());
+            $this->log('You are already at %d.', $this->schema_version->get());
             return;
         }
 
         $this->log(
-            "Currently at version %d. Now migrating %s to %d.\n",
+            'Currently at version %d. Now migrating %s to %d.',
             $this->schema_version->get(),
             $this->direction,
-            $this->target_version
+            max($this->target_versions)
         );
 
-        foreach ($migrations as $version => $migration) {
-            $this->execute($version, $this->direction, $migration);
+        foreach ($migrations as $number => $migration) {
+            list($branch, $version) = $this->migrationBranchAndVersion($number);
+
+            $action = $this->isUp() ? 'Migrating' : 'Reverting';
+            $migration->announce("{$action} %s", $number);
+
+            if ($migration->description()) {
+                $this->log($migration->description());
+                $this->log(str_repeat('-', 79));
+            }
+
+            $time_start = microtime(true);
+            $migration->migrate($this->direction);
+
+            $action = $this->isUp() ? 'Migrated' : 'Reverted';
+            $this->log('');
+            $migration->announce("{$action} in %ss", round(microtime(true) - $time_start, 3));
+            $this->log('');
+
+            $this->schema_version->set($this->isDown() ? $version - 1 : $version, $branch);
+
+            $action = $this->isUp() ? 'MIGRATE_UP' : 'MIGRATE_DOWN';
+            StudipLog::log($action, $number, $this->schema_version->getDomain());
         }
     }
 
     /**
-     * Executes a migration's up or down method
+     * Calculate the selected target versions for all relevant branches. If a
+     * single branch is selected for migration, only that branch and all its
+     * children are considered relevant.
      *
-     * @param  string $version      Version to execute
-     * @param  string $direction    Up or down
-     * @param  Migration $migration Migration to execute (optional, will be
-     *                              loaded if missing)
+     * @param mixed  the target version as an integer, array or NULL thus
+     *               migrating to the top migrations
+     *
+     * @return array an associative array, whose keys are the branch names
+     *               and whose values are the target versions
      */
-    public function execute($version, $direction, Migration $migration = null)
+    public function targetVersions($target_version)
     {
-        if ($this->isUp($direction) && $this->schema_version->contains($version)) {
-            $this->log("Version {$version} is already present.\n");
-            return;
+        $top_versions = $this->topVersion(true);
+        $target_branch = $this->schema_version->getBranch();
+  
+        if (is_array($target_version)) {
+            return $target_version;
         }
-
-        if ($this->isDown($direction) && !$this->schema_version->contains($version)) {
-            $this->log("Version {$version} is not present.\n");
-            return;
-        }
-
-        if ($migration === null) {
-            $migrations = $this->migrationClasses();
-            if (!isset($migrations[$version])) {
-                throw new Exception("Version {$version} is invalid");
+  
+        $max_version = $target_branch ? $target_branch . '.' . $target_version : $target_version;
+  
+        foreach ($top_versions as $branch => $version) {
+            if ($branch == $target_branch) {
+                if (isset($target_version)) {
+                    $top_versions[$branch] = $target_version;
+                }
+            } else if ($target_branch && strpos($branch, $target_branch . '.') !== 0) {
+                unset($top_versions[$branch]);
+            } else if (isset($target_version) && version_compare($branch, $max_version) >= 0) {
+                $top_versions[$branch] = 0;
             }
-            list($file, $class) = $migrations[$version];
-            $migration = $this->loadMigration($file, $class);
-        }
-
-        $action = $this->isUp($direction) ? 'Migrating' : 'Reverting';
-
-        $this->announce("{$action} %d", $version);
-        if ($migration->description()) {
-            $this->log($migration->description());
-            $this->log(self::mark('', '-'));
-        }
-
-        $time_start = microtime(true);
-        $migration->migrate($direction);
-
-        $action = $this->isUp($direction) ? 'Migrated' : 'Reverted';
-        $this->log('');
-        $this->announce("{$action} in %ss", round(microtime(true) - $time_start, 3));
-        $this->log('');
-
-        // Update schema version
-        if ($this->isDown($direction)) {
-            $this->schema_version->remove($version);
-        } else {
-            $this->schema_version->add($version);
         }
-
+  
+        return $top_versions;
     }
-
+  
     /**
      * Invoking this method will return a list of migrations with an index between
      * the current schema version (provided by the SchemaVersion object) and a
      * target version calling the methods #up and #down in sequence.
      *
-     * @param mixed  the target version as an integer or NULL thus migrating to
-     *               the top migration
+     * @param mixed  the target version as an integer, array or NULL thus
+     *               migrating to the top migrations
      *
      * @return array an associative array, whose keys are the migration's
      *               version and whose values are the migration objects
@@ -287,35 +287,29 @@ class Migrator
         // Load migrations
         $migrations = $this->migrationClasses();
 
-        // Determine correct target version
-        $this->target_version = $target_version === null
-                              ? $this->topVersion()
-                              : (int) $target_version;
-
-        // Determine max version (might differ from max schema version in db in
-        // development systems)
-        $max_version = min($this->topVersion(), $this->schema_version->get());
+        // Determine correct target versions
+        $this->target_versions = $this->targetVersions($target_version);
 
         // Determine migration direction
-        if ($this->target_version > 0 && $this->target_version >= $max_version) {
-            $this->direction = 'up';
-        } else {
-            $this->direction = 'down';
+        foreach ($this->target_versions as $branch => $version) {
+            if ($this->schema_version->get($branch) < $version) {
+                $this->direction = 'up';
+                break;
+            } else if ($version < $this->schema_version->get($branch)) {
+                $this->direction = 'down';
+                break;
+            }
         }
 
         // Sort migrations in correct order
-        uksort($migrations, function ($a, $b) {
-            if (mb_strlen($a) > 8 && mb_strlen($b) > 8) {
-                return $a - $b;
-            }
-            return mb_substr($a, 0, 8) - mb_substr($b, 0, 8);
-        });
+        uksort($migrations, 'version_compare');
 
         if (!$this->isUp()) {
             $migrations = array_reverse($migrations, true);
         }
 
         $result = [];
+
         foreach ($migrations as $version => $migration_file_and_class) {
             if (!$this->relevantMigration($version)) {
                 continue;
@@ -323,10 +317,15 @@ class Migrator
 
             list($file, $class) = $migration_file_and_class;
 
-            try {
-                $result[$version] = $this->loadMigration($file, $class);
-            } catch (Exception $e) {
+            $migration = require_once $file;
+
+            if (!$migration instanceof Migration) {
+                $migration = new $class($this->verbose);
+            } else {
+                $migration->setVerbose($this->verbose);
             }
+
+            $result[$version] = $migration;
         }
 
         return $result;
@@ -342,47 +341,30 @@ class Migrator
      */
     private function relevantMigration($version)
     {
-        if ($this->isUp()) {
-            return !$this->schema_version->contains($version)
-                && $version <= $this->target_version;
-        } elseif ($this->isDown()) {
-            return $this->schema_version->contains($version)
-                && $version > $this->target_version;
+        list($branch, $version) = $this->migrationBranchAndVersion($version);
+        $current_version = $this->schema_version->get($branch);
+
+        if (!isset($this->target_versions[$branch])) {
+            return false;
+        } else if ($this->isUp()) {
+            return $current_version < $version
+                && $version <= $this->target_versions[$branch];
+        } else if ($this->isDown()) {
+            return $current_version >= $version
+                && $version > $this->target_versions[$branch];
         }
 
         return false;
     }
 
-    /**
-     * Loads a migration from the given file and creates and instance of it.
-     *
-     * @param string $file  File name of migration to load
-     * @param string $class Class name to expect to be loaded from the file
-     * @return Migration instance
-     */
-    private function loadMigration($file, $class)
-    {
-        if (class_exists($class)) {
-            $migration = new $class($this->verbose);
-        } else {
-            $migration = require $file;
-            if (!$migration instanceof Migration) {
-                $migration = new $class($this->verbose);
-            } else {
-                $migration->setVerbose($this->verbose);
-            }
-        }
-        return $migration;
-    }
-
     /**
      * Am I migrating up?
      *
      * @return bool  TRUE if migrating up, FALSE otherwise
      */
-    private function isUp($direction = null)
+    private function isUp()
     {
-        return ($direction ?: $this->direction) === 'up';
+        return $this->direction === 'up';
     }
 
     /**
@@ -390,9 +372,9 @@ class Migrator
      *
      * @return bool  TRUE if migrating down, FALSE otherwise
      */
-    private function isDown($direction = null)
+    private function isDown()
     {
-        return ($direction ?: $this->direction) === 'down';
+        return $this->direction === 'down';
     }
 
     /**
@@ -433,10 +415,7 @@ class Migrator
      */
     protected function migrationFiles()
     {
-        $files = glob($this->migrations_path . '/*.php');
-        $files = array_filter($files, function ($file) {
-            return preg_match(self::FILE_REGEXP, $file);
-        });
+        $files = glob($this->migrations_path . '/[0-9]*_*.php');
         return $files;
     }
 
@@ -450,19 +429,43 @@ class Migrator
     protected function migrationVersionAndName($migration_file)
     {
         $matches = [];
-        preg_match(self::FILE_REGEXP, $migration_file, $matches);
-        return [(int) $matches[1], $matches[2]];
+        preg_match('/\b([0-9.]+)_([_a-z0-9]*)\.php$/', $migration_file, $matches);
+        return [$matches[1], $matches[2]];
+    }
+
+    /**
+     * Split a migration version into its branch and version parts.
+     *
+     * @param string  a migration version
+     * @return array  an array of two elements containing the migration's branch
+     *                and version on this branch.
+     */
+    public function migrationBranchAndVersion($version)
+    {
+        if (preg_match('/^(.*)\.([0-9]+)$/', $version, $matches)) {
+            $branch = preg_replace('/\b0+/', '', $matches[1]);
+            $version = (int) $matches[2];
+        } else {
+            $branch = '0';
+            $version = (int) $version;
+        }
+        return [$branch, $version];
     }
 
     /**
      * Returns the top migration's version.
      *
+     * @param bool  return top version for all branches, not just default one
      * @return int  the top migration's version.
      */
-    public function topVersion()
+    public function topVersion($all_branches = false)
     {
-        $versions = array_keys($this->migrationClasses());
-        return $versions ? max($versions) : 0;
+        $versions = [0];
+        foreach (array_keys($this->migrationClasses()) as $version) {
+            list($branch, $version) = $this->migrationBranchAndVersion($version);
+            $versions[$branch] = max($versions[$branch], $version);
+        }
+        return $all_branches ? $versions : $versions[$this->schema_version->getBranch()];
     }
 
     /**
@@ -479,39 +482,6 @@ class Migrator
         }
 
         $args = func_get_args();
-        vprintf(trim(array_shift($args)) . "\n", $args);
-    }
-
-
-    /**
-     * Overridable method used to return a textual representation of a stronger
-     * ouput of what's going on in me. You can use me as you would use printf.
-     *
-     * @param string $format just a dummy value, instead use this method as you
-     *                       would use printf & co.
-     */
-    protected function announce($format)
-    {
-        # format message
-        $args = func_get_args();
-        $message = vsprintf(array_shift($args), $args);
-
-        return $this->log(self::mark($message));
-    }
-
-    /**
-     * Pads and highlights a given text to a specific length with the given
-     * sign.
-     *
-     * @param string $text
-     * @param string $sign
-     */
-    public static function mark($text, $sign = '=')
-    {
-        $text = trim($text);
-        if ($text) {
-            $text = " {$text} ";
-        }
-        return str_pad("{$sign}{$sign}{$text}", 79, $sign, STR_PAD_RIGHT);
+        vprintf(array_shift($args) . "\n", $args);
     }
 }
diff --git a/lib/migrations/SchemaVersion.php b/lib/migrations/SchemaVersion.php
index bbb2a6b26a7337dc320ae21c2e818dea4844b8fe..d2cf7662d12a1cf4706aafe9a3295d571e6d80cd 100644
--- a/lib/migrations/SchemaVersion.php
+++ b/lib/migrations/SchemaVersion.php
@@ -14,32 +14,32 @@
 interface SchemaVersion
 {
     /**
-     * Returns current schema version (as maximum number).
+     * Retrieve the branch of this schema.
      *
-     * @return int schema version
+     * @return string schema branch
      */
-    public function get();
+    public function getBranch();
 
     /**
-     * Returns whether the given version is already present for the given
-     * domain.
+     * Retrieve all branches of this schema.
      *
-     * @param  int $version Version number
-     * @return bool
+     * @return array all schema branches
      */
-    public function contains($version);
+    public function getAllBranches();
 
     /**
-     * Adds a schema version.
+     * Returns current schema version.
      *
-     * @param int $version schema version
+     * @param string $branch schema branch (optional)
+     * @return int schema version
      */
-    public function add($version);
+    public function get($branch = 0);
 
     /**
-     * Removes a schema version.
+     * Sets the new schema version.
      *
-     * @param int $version schema version
+     * @param int $version new schema version
+     * @param string $branch schema branch (optional)
      */
-    public function remove($version);
+    public function set($version, $branch = 0);
 }
diff --git a/lib/models/StudipNews.class.php b/lib/models/StudipNews.class.php
index d164a4a0694d2c276850fcd8efcca5c22220477d..b528604bfbf8b27ca374ef0aaca1686e54cb5498 100644
--- a/lib/models/StudipNews.class.php
+++ b/lib/models/StudipNews.class.php
@@ -109,9 +109,6 @@ class StudipNews extends SimpleORMap implements PrivacyObject
 
     public static function CountUnread($range_id = 'studip', $user_id = false)
     {
-        if (!DBSchemaVersion::exists('studip', '20210201')) {
-            return 0;
-        }
         $query = "SELECT SUM(nw.chdate > IFNULL(b.visitdate, :threshold) AND nw.user_id != :user_id)
                   FROM news_range a
                   LEFT JOIN news nw ON (a.news_id = nw.news_id AND UNIX_TIMESTAMP() BETWEEN date AND date + expire)
diff --git a/lib/plugins/engine/PluginManager.class.php b/lib/plugins/engine/PluginManager.class.php
index 13973592e0b4c83031249f5e17bbfb71facee4dc..5f67c40aad264653e56b0113d2ea6cf81d88bf92 100644
--- a/lib/plugins/engine/PluginManager.class.php
+++ b/lib/plugins/engine/PluginManager.class.php
@@ -192,10 +192,6 @@ class PluginManager
      */
     public function isPluginActivated ($id, $context)
     {
-        if (!DBSchemaVersion::exists('studip', '20210201')) {
-            return null;
-        }
-
         if (!$context) {
             return null;
         }
diff --git a/tests/unit/lib/classes/test-migrations/10_test_migration_ten.php b/tests/_data/migrations/10_test_migration_ten.php
similarity index 100%
rename from tests/unit/lib/classes/test-migrations/10_test_migration_ten.php
rename to tests/_data/migrations/10_test_migration_ten.php
diff --git a/tests/unit/lib/classes/test-migrations/1_test_migration_one.php b/tests/_data/migrations/1_test_migration_one.php
similarity index 100%
rename from tests/unit/lib/classes/test-migrations/1_test_migration_one.php
rename to tests/_data/migrations/1_test_migration_one.php
diff --git a/tests/_data/migrations/2.1_test_migration_two_one.php b/tests/_data/migrations/2.1_test_migration_two_one.php
new file mode 100644
index 0000000000000000000000000000000000000000..13126d41ec0dd7ccd06182de7fdfc4b334a8b812
--- /dev/null
+++ b/tests/_data/migrations/2.1_test_migration_two_one.php
@@ -0,0 +1,4 @@
+<?php
+class TestMigrationTwoOne extends Migration
+{
+}
diff --git a/tests/unit/lib/classes/test-migrations/2_test_migration_two.php b/tests/_data/migrations/2_test_migration_two.php
similarity index 100%
rename from tests/unit/lib/classes/test-migrations/2_test_migration_two.php
rename to tests/_data/migrations/2_test_migration_two.php
diff --git a/tests/unit/lib/classes/MigrationTest.php b/tests/unit/lib/classes/MigrationTest.php
index 7085c9ee9d7f09456bc9f3192412029e36f116c2..1f0bc98c56b320f52af4499d411cb1c6f9c34f06 100644
--- a/tests/unit/lib/classes/MigrationTest.php
+++ b/tests/unit/lib/classes/MigrationTest.php
@@ -13,9 +13,7 @@ class MigrationTest extends \Codeception\Test\Unit
 
     public function setUp(): void
     {
-        $this->before = isset($GLOBALS['CACHING_ENABLE'])
-                      ? $GLOBALS['CACHING_ENABLE']
-                      : null;
+        $this->before = $GLOBALS['CACHING_ENABLE'] ?? null;
         $GLOBALS['CACHING_ENABLE'] = false;
 
         require_once 'lib/classes/StudipCache.class.php';
@@ -41,30 +39,26 @@ class MigrationTest extends \Codeception\Test\Unit
     {
         return new class() implements SchemaVersion
         {
-            private $versions = [];
+            private $versions = [0];
 
-            public function get()
+            public function getBranch()
             {
-                return count($this->versions) > 0 ? max($this->versions) : 0;
+                return 0;
             }
 
-            public function contains($version)
+            public function getAllBranches()
             {
-                return in_array($version, $this->versions);
+                return array_keys($this->versions);
             }
 
-            public function add($version)
+            public function get($branch = 0)
             {
-                if (!$this->contains($version)) {
-                    $this->versions[] = $version;
-                }
+                return $this->versions[$branch];
             }
 
-            public function remove($version)
+            public function set($version, $branch = 0)
             {
-                if ($this->contains($version)) {
-                    $this->versions = array_diff($this->versions, [$version]);
-                }
+                $this->versions[$branch] = (int) $version;
             }
         };
     }
@@ -72,29 +66,11 @@ class MigrationTest extends \Codeception\Test\Unit
     private function getMigrator($schema_version = null)
     {
         return new Migrator(
-            __DIR__ . '/test-migrations',
+            TEST_FIXTURES_PATH . 'migrations',
             $schema_version ?: $this->getSchemaVersion()
         );
     }
 
-    public function testSchemaVersion()
-    {
-        $schema_version = $this->getSchemaVersion();
-        $this->assertSame(0, $schema_version->get());
-
-        $schema_version->add(1);
-        $this->assertTrue($schema_version->contains(1));
-        $this->assertSame(1, $schema_version->get());
-
-        $schema_version->add(2);
-        $this->assertTrue($schema_version->contains(2));
-        $this->assertSame(2, $schema_version->get());
-
-        $schema_version->remove(1);
-        $this->assertFalse($schema_version->contains(1));
-        $this->assertSame(2, $schema_version->get());
-    }
-
     public function testRelevance()
     {
         $migrator = $this->getMigrator();
@@ -102,7 +78,7 @@ class MigrationTest extends \Codeception\Test\Unit
         $relevant = $migrator->relevantMigrations(null);
         $this->assertSame(4, count($relevant));
 
-        $migrator->migrateTo(10);
+        $migrator->migrateTo(2);
 
         $relevant = $migrator->relevantMigrations(null);
         $this->assertSame(1, count($relevant));
@@ -113,7 +89,7 @@ class MigrationTest extends \Codeception\Test\Unit
         $schema_version = $this->getSchemaVersion();
         $migrator = $this->getMigrator($schema_version);
         $migrator->migrateTo(null);
-        $this->assertSame(20190417, $schema_version->get());
+        $this->assertSame(10, $schema_version->get());
         $this->assertSame(0, count($migrator->relevantMigrations(null)));
 
         return $schema_version;
@@ -133,13 +109,12 @@ class MigrationTest extends \Codeception\Test\Unit
     public function testGaps()
     {
         $schema_version = $this->getSchemaVersion();
-        $schema_version->add(2);
-        $schema_version->add(10);
+        $schema_version->set(10);
 
         $migrator = $this->getMigrator($schema_version);
 
         $relevant = $migrator->relevantMigrations(null);
-        $this->assertSame(2, count($relevant));
-        $this->assertEquals([1, 20190417], array_keys($relevant));
+        $this->assertSame(1, count($relevant));
+        $this->assertEquals(['2.1'], array_keys($relevant));
     }
 }
diff --git a/tests/unit/lib/classes/test-migrations/20190417_returns_instance.php b/tests/unit/lib/classes/test-migrations/20190417_returns_instance.php
deleted file mode 100644
index c55e214167994d386e47fbb530e5b05ec836e9f7..0000000000000000000000000000000000000000
--- a/tests/unit/lib/classes/test-migrations/20190417_returns_instance.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php
-return new class() extends Migration
-{
-
-};