Share →
Buffer

Let’s talk subtrees. This, to me, is a close tie with bisect for the most impressive tool Git has out there. Subtrees allow you to have subprojects within a subdirectory of the main project. You can commit them with the parent directory, make a branch from subtrees, and even do subtree merges with the parent directory. Subtrees have been available in the stock version of Git since version 1.7.11, May 2012.

There have been comparisons of subtrees to submodules, but for most cases, subtrees take the cake:

  • You can make commits to the parent directory (main project) with the subtree’s history, or you can make a change just in the subtree and push its changes independent of the parent directory up to its own repository.
  • Once you push a commit up to the remote of the parent directory, the subtree comes with it. So if we clone the parent directory’s remote, we will also automatically get the subtree with it.
  • Simple to use, no need to learn anything new (unless you count commands with “subtree” before them), and you can just ignore the subtrees as well.

So let’s walk through a scenario: We have a team of developers who are working on a Console Application in C# called Greetings that greets a user in different languages. Its repo is located both on Visual Studio Online and our local machine as GreetingsRepo. It has a dependency, SayHi, that does the heavy-lifting of translations. SayHi also has repos on Visual Studio Online and locally, SayHiRepo. What we ultimately want to do is allow others developers to clone GreetingsRepo and already have SayHi in the project so that they can improve Greetings or even just make changes in SayHi and push them to SayHiRepo. We’ll use subtrees to make our local SayHiRepo a subtree for GreetingsRepo. A diagram of the two repos is shown below:

Before:

s1

After:

s2

s4

s5

First, let’s go into our local GreetingsRepo and add the SayHiRepo that is on Visual Studio Online (we will call it SayHiRemoteRepo) as a remote so that it’s less typing to push/pull:

$ git remote add <remote name> <remote URL>

s7

Next, we will add a subtree to Greetings with an add command:

$ git subtree add –-prefix=<new folder> <remote> <branch>

s8

One thing to note is that all of the history in SayHiRepo is also pulled down into our repo. If you’d rather have a cleaner history, you can squash all of the commits into one single commit by using the –-squash command when adding the subtree instead (we are keeping the history as-is in our example):

$ git subtree add –-prefix=<new folder> <remote> <branch> –-squash

 s9

Almost by magic, the solution file for SayHi is now in our Greetings project! The subdirectory appears as a folder in the repo (next to our Greetings project). We can make edits to either solution.

s10 s11

If we make changes in Greetings.cs and SayHi.cs, commit to the parent directory, and push to GreetingsRepo on Visual Studio Online, the changes and the subtree (as well as its history) will appear there:

s12

Note: I would recommend creating a branch before making changes in your subdirectory (to avoid making master complicated).

But what if I wanted to just make a change in SayHi.sln and push that change up to SayHiRemoteRepo? You can with subtree push. If I make a change just in SayHi.cs and make a commit in the parent directory, we can then push using:

$ git subtree push –-prefix=<subtree folder> <remote> <branch>

s13

The coolest thing about subtree push is that it automatically filters or “splits” the commits that don’t relate to the sub-directory. So it only pushed to SayHiRemoteRepo the changes that dealt with SayHi (including the commit that changed Greeting.cs and SayHi.cs):

s14

To update our subdirectory, SayHiRepo, we pull changes from our remote, SayHiRemoteRepo:

$ git subtree pull –-prefix=<subtree folder> <remote> <branch>

 s15

So did subtrees work if we want to clone our main project and have a subdirectory included in it? Take a look:

 s16

s17

And there you have it, friends. Subtrees are a great way to maintain good workflow, cause little disturbances in your main project, and are simple to use. Try it out for yourselves!

Print Friendly
Tagged with →  
  • Aaron

    Say the sub-solution’s projects have Nuget package dependencies; given when working on the SayHi solution alone, the packages would typically land in the .packages directory of the sub-solution’s root. Therefore the SayHi projects would typically make assembly file references with the ..packages relative path.

    Now, when opening with the super solution, the solution root is now two levels apart. The SayHi projects would have to adjust themselves to reference ….packages wouldn’t they? How to reconcile the different depth levels of the different solution files?

  • Aaron

    Furthermore, supposing we set this up in the “master” development branch, which ties up to the master branch of the sub repo as well.

    But say when working a separate Test branch, is it easy to correspondingly switch the sub repo directory to also reference its Test branch instead of the master branch?

  • Aaron

    Regarding the different solutions’ relative paths to Nuget packages folder, I found that hacking each project’s .*proj file to use the $(SolutionDir) variable works

    instead of ..packages for the hint path,

    apply as $(SolutionDir)packages

  • Sachi Williamson

    Nice hack, Aaron, that’s a good option to manage those dependencies using subtrees. With your other question about branches, the nice part about subtrees is that it is just a normal folder when you use the git subtree command, so one way that a StackOverflow user suggested is to delete it and recreate the subtree from the new branch, but that also depends on commits in the subtree vs. the subtree’s remote (like SayHiRemoteRepo).

    I also think that it’s a good idea to create the subtree in the master branch, then branch off of master after that commit so that you can keep your subtree. Once you branch out of master after that commit, you can then make changes in the subtree in that new branch and commit in that new branch. You may also want to consider using the squash command when adding the subtree, this may help keep history between the subtree and your master branch cleaner. Hope this helps, please let us know if you have any other questions.

  • Aaron

    Ok the way to manage this branch-synchronisation issue is to simply pull the corresponding external repo branch in when working on the local repo’s branch.

    Say when switching over to the Test branch, pull the subtree with Test branch

    git checkout Test
    git subtree pull –squash –prefix=External External Test

    or via tag version

    git subtree pull –squash –prefix=External External Release_v1.0