Migrating a language project to a new version of MPS usually involves touching almost all module files in a project. This increases the probability that it will conflict with another parallel change. In a large project, the migration had better happen quickly, to avoid disturbing the rest of the team. At the same time, a large migration is unlikely to succeed without preparation.

How can you prepare and execute a migration without blocking the team for a significant amount of time? The key is to prepare a migration on a separate branch and then update the branch as new changes are committed to the main branch.

However, simply regularly merging the main branch into the migration branch from time to time is not going to work because merging migrated and non-migrated code should be avoided. It can work if you are careful and review the changes but it’s not something you want to be doing regularly.

Instead, I found the rebasing capabilities of Git very helpful in this situation.

To prepare a migration, I create a separate branch off of the main branch. The first commit I make to this branch updates the versions of dependencies in my build script. Then I try building the project with the new versions but not migrated yet. Usually this almost works but a few errors pop up that I can fix (in a separate commit).

Next, I make sure that I have no uncommitted changes and I run the migration wizard. Usually it will find something wrong with the project and I abort the wizard to review the problems it found. Although the wizard was aborted, it often will have made some changes to the files, such as updating the cached names of reference targets (“resolveInfo”) and information about versions of languages used (“languageVersions” and “dependencyVersions” sections in module files).

I commit these changes to get them out of the way. Then I focus on fixing the issues the wizard has found and make a separate commit (or several) with the fixes.

At some point I run the migration assistant and execute the migrations. If everything goes well, I commit after the wizard completes.

The project may need additional fixes after the migration. I make separate commits for those fixes as well.

At this point I have a branch that is fully migrated, with all issues fixed. Now, it often happens that while I was busy migrating, somebody has changed the main branch. With Git I can quite easily rebase the migration branch on top of the latest changes in the main branch. I just need to redo the automated changes rather than having Git merge them.

To redo changes in Git, I use interactive rebase (git rebase -i) and tell Git to drop the commit with the migration and do a break immediately after. Here is what a typical interactive rebase script might look like:

# Before migration: pick all the changes
pick 123456789 Update dependency versions
pick abcdefabc Fix errors caused by new dependency versions
# Migration: drop and break
drop 111111111 Migration wizard saves files before migration
break
pick 222222222 Fix issues preventing migration
drop 333333333 Migrate the projects
break
# 
pick 987654321 Fix issues after migration
pick abcd12345 More fixes

When the rebase process breaks at one of the designated points, I perform the automated change in MPS, commit it, and continue rebasing.

With the help of Git rebase I am thus able to prepare a migration in advance, yet update it safely as needed.

Caveats

  • If a migration produces new nodes rather than changing the concepts of existing nodes, the identifiers of the new nodes may differ from one execution of the migration to the next. This may lead to conflicts if any later commits (such as post-migration fixes) touch these nodes. This is however usually not the case.
  • If a branch is started from non-migrated code and is not merged before the migration, then at some point non-migrated code will have to be merged with the migrated code. In busy projects this may be unavoidable. On the other hand, this can be made to work if done carefully. Writing re-runnable migrations and following other best practices for evolving your languages will help, as you can re-execute migrations from the Migration menu after you merge the branches.