Git Submodules: Tips for JetBrains IDEs

· Read in about 7 min · (1397 Words)

Do you use JetBrains IDEs like Rider? They’re pretty great; I use Rider myself daily for Unreal Engine development. In this post, I’m going to assume other JetBrains IDEs function largely the same as Rider in this respect, which I’m pretty sure they do.

When I started using Rider, I was surprised to discover that, as far as I’ve seen, JetBrains have the best IDE version control tools in the business. I’ve never been a fan of IDE-hosted version control tools, I always felt they were ill-fitting, to the extent that I even went so far as to write my own Git (and Mercurial!) tooling at one point. So, for an IDE-hosted version control system to impress me is, well, impressive. 😄

There are lots of reasons why I think they’re so good but I’m only going to focus on one in this post: submodule support.

I’m a heavy user of Git Submodules. They get a bad rap for being fiddly, and they sometimes are. But when you want to embed a shared source dependency in your project, and be able to make changes to it, and control exactly what version is used and when its updated, I haven’t found anything better. Once it’s set up, tooling really helps avoid most of the common day-to-day mistakes that make people swear off submodules.

Rider has some nice features to manage submodules day-to-day, but they don’t expose them that well. In this post I hope to show you how you can navigate having many submodules in your project and keeping it all together.

Starting point

I’m going to assume as a starting point that you’ve already got submodules set up in your repository. Unfortunately Rider’s one blind spot is that doesn’t have any good tools to set up your submodule in the first place, so you’re going to have to do that the old fashioned way.

Luckily, it’s usually quite simple; for example let’s say you’re in a console in the root of your repository, adding my library SPUD to your project can be:

git submodule add Plugins/SPUD

That’s it, you’ve now got a copy of SPUD in Plugins/SPUD, and it’s a full Git repo that you can alter and commit to separately from your own project.

Your own project will track the exact commit of this submodule, so you always know what you’re getting and updating it to a new version is entirely at your discretion.

OK, that’s all we need to do on the command line. Back to Rider / your JetBrains IDE of choice.

Directory Mappings

So, immediately Rider is going to understand that submodule. It’ll want to commit the .gitmodules file which will include that added submodule URL / path combination, and if you ever update the submodule to a different commit, it will prompt you to commit that change to the parent project. All good.

However, what it won’t do right now is expose a lot of its best tools for managing that submodule over time. To get the most out of it, we need to set up Directory Mappings for our submodule(s).

Directory mapping settings

You need to go to Settings, and navigate to Version Control > Directory Mappings. There will just be one entry for the root of your project by default. We want to add one entry for each of our submodules (which we’ve already checked out).

To do this, click the “+” option, select “Directory”, and pick the root of the submodule path. As you can see, I’ve got quite a few in this project. Confirm the settings and go back to your project.

Why? What did this do?

It’s basically telling Rider that instead of a single self-contained project, we’ve got a number of Git root directories in our project, and we want to be able to deal with them all to the fullest extent.

There are 3 main benefits directory mappings:

1. Pulling changes to submodules

If someone else has made changes to a submodule, you won’t get those changes automatically. This is because your project tracks an exact commit of the submodule, not a branch, so your version of that dependency stays locked to that commit regardless of what other development goes on. This is good! It means you don’t accidentally get changes when you don’t want them.

But, when you do want to update a dependency, it’s good to have a GUI option to do so. Now that directory mappings are set up, the Rider “Pull” window has an additional option:

Pulling with directory mappings

That drop-down on the left, where I’ve selected Plugins/SPUD, lists all the roots set up in the directory mappings window; so your own project and all the submodules. Pulling a submodule will update your local copy of the submodules files, and also the commit which your project is tracking (which you’ll commit if you like the update, more on that later).

2. Log Filtering

I really like Rider’s git log UI. One really nice thing about it is that you can add as many tabs as you like, all filtered depending on what kind of history you want to see. This button on the toolbar adds extra log tabs:

Add log tab

One of the filters is to constrain changes to certain paths, but when you’ve set up Directory Mappings, you get some easy, colour-coded checkboxes for each entry in that list, so it’s super-easy to just show the log for each of them:

Log path drop down

Notice how I’ve checked just SPUD here, so this tab becomes my SPUD history. I keep one log tab just for my own project, and one tab for each submodule that I either make changes to myself, or update often, so I can keep tabs (heh) on their histories.

Log example

Does this mean you can check any number of those boxes in the path drop-down and have a combined log for both your own project and all submodules in a single log view? Yes! However, a mixed history like this is total madness IMHO. You do you, though. 😄

3. Easily identify changes in Submodules

If your dependency is one you make changes to, either because it’s yours or you have your own custom fork, it’s handy to be able to make changes here, within your own project context, but for the changes to be sent to the shared dependency repo not your own project. That’s one of the main benefits of submodules.

When you make a change to a file in a submodule, Rider won’t show you that very usefully unless you’ve set up the Directory Mappings. Without it, you just get a single generic modified marker on the submodule folder and that’s it.

But, if you set up a directory mapping for it, changes inside the submodule show up as a regular file change but in a different root to your main project:

Change in a submodule file

Notice the colour coding (Blue) on the parent folder, which matches the colour of the boxes in the log filter drop down.

You can then commit these changes to the submodule, and push them up to the submodule repo, thus adding a new change to the shared dependency repo for any project using it to update to if they want. In the same way as the pull dialog above, you get a choice of which root your git push affects.

Once you make a new commit (or pull other people’s changes), the submodule entry in your own repo will show as updated because the commit it’s tracking will have changed. You can then commit that change to the tracked commit to your own repo:

Change a submodule tracked commit

Notice the colour code is Purple here, that’s my own project repo (it also has the name in text but I’ve obscured that).

This is actually how any submodule file change appears if you don’t set up the Directory Mappings, hence why it’s not very useful. Once you’ve set up Directory Mappings the difference between a file change in the submodule, and change to the tracked commit in the parent repo, is much clearer.


So there you go; I realised when setting up a new project that I’d forgotten about this Directory Mapping setup step, and how much useful functionality it adds to submodule usage in JetBrains IDEs. Rather than just make some private notes for next time, I figured I’d write up a blog post in case someone else found it useful. I hope you did!