Exploring Two-Way Sync Between SVN and Plastic SCM
Published: 2020-05-13 06:40:19

I'm often asked by customers to do work on some project that is being tracked in SVN.  Having been spoiled by Plastic SCM's very well-thought-out system, SVN's crufty paradigm soup of fragility and slowness is mind-numbingly infuriating.  I looked into what I could do to work with Plastic on my end without any inconvenience or interruption to the customer (that is to say, without creating any _additional_ inconvenience or interruption beyond that which is already imposed upon them by SVN).

The simplest approach is, of course, to point an SVN client and a Plastic client to the same workspace folder, managing back-and-forth manually:  Pull latest in SVN, commit in Plastic; merge to main in Plastic, commit in SVN.  I thought it would be worth exploring the possibility of a solution that would actually maintain the entire history of the SVN repository in Plastic, so that I can see `annotate` history in my IDE, see incremental Blueprint change diffs in Unreal Engine, etc.  Really, so that I can use it as proper and fully-featured version control rather than just a file transfer mechanism with an undo feature.

I quickly found [Plastic's guest blog post about migrating from SVN using git-svn and then Git Sync](http://blog.plasticscm.com/2015/09/moving-from-svn-to-plastic-scm-how-we.html) (Git Sync being Plastic's built-in feature to synchronize between Plastic and Git).  I started following the steps in the post, but found that `git svn` was a bit too cumbersome - there were a lot of things that left me thinking, _"this could really use a script to simplify the process and improve the default behavior"_.  I figured, if _I_ was thinking that, then **somebody** has probably already thought the same thing and created those scripts.

I found svn2git (in particular, [this implementation](https://github.com/crazy-max/docker-svn2git-mirror) of svn2git in a Docker container with a nice config file and automatic recurring sync).  How about that?  Not only had somebody already written the scripts, but they happen to be my crazy internet doppleganger.  I didn't even know I had one!  Touche, _Crazy Max_, touche.

After making several fixes to Crazy's setup, and modifying it to use `git://` instead of `ssh://`, I have SVN to Git to Plastic working very well.  The `svn2git` container is configured to push each repo to a `git daemon` container (eventually I would like to figure out how to avoid this data redundancy - to have the `git daemon` use the same `.git` folders as `svn2git` - but currently one seems to require bare while the other requires working copy).  I then use Git Sync from the Plastic GUI (I plan to automate this at an interval, too).

I realized that I could ditch the `git daemon`, and instead use [Plastic GitServer](https://www.plasticscm.com/gitserver), configuring `svn2git` to push directly to that, instead.  Having read that [Plastic Git Server maintains two copies of the data](https://www.plasticscm.com/documentation/gitserver/plastic-scm-version-control-gitserver-guide#Storagerestrictions) (Git format and Jet format, I assume), I decided to stick with the `git daemon` middleman in favor leaving open the possibility of consolidating the `git daemon` and `git svn` "databases" (I doubt I can have `git svn` and Plastic GitSync use the same .git files as easily).

Once I had this working, the next step was to figure out how I would get data going back in the other direction.  My team prefers [branch-per-task](https://www.plasticscm.com/book/#_one_task_one_branch), but most SVN-using customers tend to prefer big commits to trunk as the default mode of operation.  Again, there is a very "straightforward" solution: I can point SVN to the same workspace and commit to it while Plastic is on the feature branch;  but even if the customer doesn't care about task branches and granular commits, wouldn't it be much better if I could see those merges in Plastic?  Especially so with Plastic GUI's feature to show merged vs unmerged branches as different colors in the graph view.

So, let's say that I merge our task branch into `/main`, sync that to `master` in the intermediate Git repo, `pull` it into the `git svn` working copy, and then finally `dcommit` it to SVN.  Given that Git Sync syncs everything without any ability to filter branches, I must expect that my task-branches and merge-commits will be synced _as such_ into Git.  This is an unfortunate waste of hard disk space, but it's not a dealbreaker as long as the Git-to-SVN push treats the merge-commit as a regular one.  After [reading about `git svn dcommit`'s behavior when merges are present](https://stackoverflow.com/questions/6214711/git-log-and-show-on-a-bare-repo), it appears that this is exactly how `git svn` behaves by default - yay!  However, this action will _replace_ the original merge-commit with a new one.  My past experience tells me that replacing commits in Git causes Plastic to fail subsequent Git Sync operations unless the conflicting changeset is removed from Plastic.  I was still willing to give it a shot, however...

At some point in the past, some error message that I got from Plastic brought to my attention the existence of `%LOCALAPPDATA%\plastic4\sync\git`.  I poked around in there and found that `cset.mapping.conf` contains very straightforward association between Plastic changesets and Git commits.  I figured I could potentially work around any issues by replacing the mege-commit git-hash with the svn-rebase git-hash in this file.

To my surprise, however, I didn't end up needing to touch those hidden files.  When I ran a Git Sync after everything else, Plastic brought the replacement merge commit in alongside the existing one.  The resulting forked head prevented it from pushing the first merge-commit back again.  I was surprised again to see that the replacement commit appeared with an identical merge link!  I didn't expect this having read that `git svn dcommit` destroys merge links, but it seems that the Git community has fixed that shortcoming.  At that point, all I had to do was to delete the original merge-commit, leaving the replacement one.

I'm left with a working bi-directional system.  It will require a bit of additional automation before I'm totally satisfied with it, but it's already sufficient to begin saving myself and my team some time and agony.