tl;dr (see table)

Test Result
Link the library project into the project’s Assets folder, without remote git repo? Doesn’t work: For submodules we need a remote-origin
Link the library project into the project’s Assets folder with remotes repo? Doesn’t work: Unity doesn’t like a complete sub-project in the Assets folder.
Maybe git-subtree does the trick? Doesn’t work: Subtree is more or less an import and not a link.
Use the new Unity PackageManager features? Doesn’t work: Complicated and doesn’t work with older Unity versions
Use a submodule and a symbolic link? It works!

About

Submodules can be a great tool for sharing library code and I like to make use of them when working with Unity. In this article I share my notes on my experiments around git submodules in Unity projects.

Git Submodule Project model for Unity
Git Submodule Project model for Unity

The development model I have in mind looks like this: I like to have on or more product project sharing one or more library project and want to able to commit, push and pull in each of them. Also I want to have reproducible connections between the project and the library code. So if I check out an old commit of a product it shall checkout the matching library project commit as well. All this without Unity plugins or the need of none-git work before or after the code change. No Unity packaging. No extra steps when committing.

To use this model one has to solve one issue: Unity (in it’s current state) forces the game creator to put all source-codes and assets into one folder named: Assets. As I had several ways in mind how I could solve this I took the liberty to write down my approaches and share them here.

I’m using Unity 2017.4.11f1, macos 10.13.6 and Sourcetree 3.0.0.

Test 1: Link library project into the projects asset folder (no remotes)

Approach

The test case is made of one product project and one library project. Both are complete Unity projects. In this approach I like to see if I can simply link the library project as a sub-folder into the Assets folder of the product project.

Being lazy I try this without using a remote git-repo.

The initial structure looks like this:

Lib1
  - ProjectSettings
  - Assets
    - lib1_asset.txt
  - lib1_file.txt
  - .gitignore
Proj1
  - ProjectSettings
  - Assets
    - proj1_asset.txt
  - proj1_file.txt
  - .gitignore  

First I want each project to be in it’s own repository. Then I like to inject lib1 as a submodule into proj1:

The result should look something like this:

Lib1
  - ProjectSettings
  - Assets
    - lib1_asset.txt
  - lib1_file.txt
  - .gitignore
Proj1
  - ProjectSettings
  - Assets
    - proj1_asset.txt
    Lib1 // as submodule
      - ProjectSettings
      - Assets
        - lib1_asset.txt
      - lib1_file.txt
      - .gitignore
  - proj1_file.txt
  - .gitignore

Setup

To set this up we first create the project structure. Then we need to create the repositories in each folder. I use Sourcetree for this.

For each folder:

  1. Create a repo
  2. Create a .gitignore
  3. Commit (master branch)

Now we have two repos with one commit in each.

The next step is to link lib1 into the main project. For that I do in Sourcetree this:

  1. Open proj1
  2. Right-Click on submodules -> Add Submodule
  3. Source Path: Navigate to the root folder of lib1
  4. Set local relative path (in proj1): Asssets/lib1
  5. Say “okay”
  6. Two modified files appear in proj1 (they are already staged)
  7. We need to commit the changes

Basically that’s it. Now the library project is part of proj1.

Be careful: If you create a submodule in Sourcetree it automatically stages the files for you. You only have to commit them. Never unstage the initial submodule files. I incidentally unstaged them and created an incompatible state and I had to setup everything again.

Testing

Let’s see how this setup works and do some tests.

  • Use case: modify a file in lib1 (the lib project itself not in the submodule)
    • Edit lib1_file.txt
    • The change is visible in lib1
    • I commit the change in lib1
    • Let’s see if we can get that into proj1/lib1
    • proj1/lib1 shows one pull request.
    • Pulling brings the change into proj1!
  • Use case: modify a file in proj1/lib1
    • Edit lib1_file.txt
    • The change is visible in proj1 (submodule changed)
    • The change is also visible in proj1/lib1, in the submodule.
    • I can commit the change in proj1/lib1, in the submodule.
    • However, I can’t push.
    • Reading the error message it’s because the origin is a bare repository. Bare Repository. Welp. I don’t really get it, but what I get that it is related to the fact that I don’t use a remote origin.

Result

  • I got exactly the desired structure.
  • I can make changes in the lib1 project.
  • I can read all changes in proj1/lib1
  • I cannot write changes from proj1/lib1 to lib1 (commit: yes, pull: no)
  • For submodules we need an remote-origin

Test 2: Link library project into the project’s asset folder (with remotes)

Approach

Create proj1, lib1 and create a remote repository for both of them. Then basically check if we get around the issue of Test 1.

Setup

Generate the following structure:

Lib1 // with remote origin
  - ProjectSettings
  - Assets
    - lib1_asset.txt
  - lib1_file.txt
  - .gitignore
Proj1 // with remote origin
  - ProjectSettings
  - Assets
    - proj1_asset.txt
    Lib1 // as submodule
      - ProjectSettings
      - Assets
        - lib1_asset.txt
      - lib1_file.txt
      - .gitignore
  - proj1_file.txt
  - .gitignore
  1. We create both as local repositories
  2. We create the counter parts on a remote git
  3. We combine the projects by setting up the remote repository
  4. Now we add lib1 to proj1, however this time the remote repo
  5. We commit the changed module files. DON’T UNSTAGE HERE ;)
  6. We commit the changes to the remote as well

That’s it. Now let’s test.

Testing

  • Use case 1: modify a file in lib1 (the lib project itself not in the submodule)
    • changed lib1_file.txt in lib1 folder
    • comitted the change
    • pushed the change
    • proj1/lib1 does not show the change. Which is what we want.
    • opening the project proj1/lib1 shows that we can pull a change
    • this changed the proje1/lib1 submodule. This makes sense as our pull changed the commit-pointer we have in proj1.
    • we commit and push that submodule too
    • Now we have the modified file in the origin as well
  • Use case 2: modify a file in proj1/lib1
    • changed lib1_file.txt in proj1/lib1 folder
    • The change becomes visible in proj1 lib1 submodule. 3 little dots.
    • We open proj1/lib1. It shows the modified content
    • We commit the changes
    • We push it to the origin.
    • This time it works!
    • Going back to proj1/lib1 we see the submodule has changed
    • We commit that and we are done in proj1.
    • If we check in lib1 we can see the modification in the origin.
  • Use case 3: Test in unity
    • Open proj1 in Unity
    • Unity says: You are trying to import an asset which contains a global game manager. This is not allowed.

Results

  • Git submodule setup works.
  • We can have two repositories, link them together and work in both of them.
  • We can modify content in the library and also in the project.
  • Both can fetch from each other.
  • This supports a structure where we can have one or more projects share some nice generic libraries that we use a lot and update from time to time.
  • Unity doesn’t like a complete sub-project in the Assets folder.
  • The UnityPackageManager or ProjectSettings cause this to fail. If we had a setup which would ignore those it would work.

Test 3: Maybe git-subtree does the trick?

Approach

Using submodules we can import an entire library project into another project. However, our library folder is packed with the entire Unity project. In Test 2 I found out Unity doesn’t like that at all.

My next thought was: “maybe we can create a submodule-link that points only to a subfolder of the library”. But from what I read it seems we can’t use a subfolder in git-submodules.

There is another option in git called Subtrees. Maybe they can help? In this test I will check if it is possible to use a linked subtree to only the get Assets content of lib1 into proj1 and still being able to update content for both.

Setup

The result should look like this:

Lib1 // with remote origin
  - ProjectSettings
  - Assets
    - lib1_asset.txt
  - lib1_file.txt
  - .gitignore
Proj1 // local
  - Assets
    - proj1_asset.txt
    - Lib1 // subtree from remote
      - lib1_asset.txt
  - proj1_file.txt
  - .gitignore  
  1. We create a new parent project: proj1
  2. We set it up as a local git repo
  3. We do the initial commit
  4. We add the remote lib1 as subtree: rightclick on submodule -> Add/Link Subtree
  5. We select the lib1 remote project with the Asset path: https://.git/Assets
  6. Squash commit? https://stackoverflow.com/a/35704829. Not now.

Testing

  • Doesn’t work? I can’t find any content in the subtree folder
  • While reading into this. I realized it’s the wrong tool.

Results

  • Bad approach. Subtree’s isn’t the right tool here:
  • Subtree is more or less an import and not a link. (details.)
  • Too bad :(

Test 4: Use the new Unity PackageManager features?

Approach

Consider Unity Package manager.

Result

  • Hm… reading into all this I’m not really motivated to follow that approach.
  • Also I need a solution that works in older Unity versions too (some clients have this requirement).

Test 5: Use a submodule and a directoy link

Approach

In this test I like to follow the approach from prime31. I setup the submodule in proj1. However, I don’t put it into the Assets folder. Instead we use a folder next to Assets. Then we link the content of that directory as a symbolic link into the Assets folder.

Setup

We create two Unity projects.

Lib1
  - ProjectSettings
  - Assets
    - lib1_asset.txt
  - lib1_file.txt
Proj1
  - ProjectSettings
  - Assets
    - proj1_asset.txt
  - proj1_file.txt

Then we link the entire lib1 project into a subdirectory and link the content of lib1. Like this:

Proj1
  - ProjectSettings
  - Assets
    - proj1_asset.txt
    - Lib1 // this is linked to (A)
      - lib1_asset.txt
  - proj1_file.txt
  - Submodules
    -Lib1
      - ProjectSettings
      - Assets // (A) the project content
        - lib1_asset.txt
      - lib1_file.txt
  1. Create two Unity projects
  2. Create the local git repositories
  3. For lib1 we create and push it to an remote-origin
  4. We create a submodule folder in proj1
  5. We initialize lib1 as submodule in proj1/submodules
  6. Now we link the folder of lib1 assets using:
# On macos/linux
cd ~/test6/proj1/Assets
ln -s ../submodules/lib1/Assets Lib1

# if succeeded we can now go into:
cd ~/test6/proj1/Assets/Lib1

# On windows:
# TODO !!

Testing

  • Creating link succeeded
  • The structure looks as expected
  • Using pwd in proj1/Assets/lib1 looks alright
  • Open proj1 in Unity works without issues
  • Sourcetree shows proj1/Assets/Lib1 as a new folder. Which is fine
  • Committing proj1/Assets/Lib1 works without issue
  • Now let’s edit a file in lib1, commit and push it
  • Let’s see if we can pull the change in proj/lib1
  • Open proj1/lib1 in Sourcetree shows that we can pull something
  • Pull proj1/lib1
  • Checking Sourcetree it works perfectly!
  • Checking Unity it works perfectly!
  • In Sourcetree I see the changed submodule-link
  • In Unity I see the updated file
  • Now let’s check if we can modify a file in proj1/Assets/Lib1
  • In Sourcetree it shows modified submodule content. Perfect!
  • Open proj1/lib1 in Sourcetree shows the modified file. Let’s commit that

Result

  • It just works

Final words

Using symbolic links we have fully working solution: Git-submodules in Unity with only a little headache. We can work on one or more library from multiple projects.

The only downside is a little extra work every time we add or remove a submodule to update the symbolic link. This introduces some problems when it comes to automatic building. Also it requires us to consider platforms. On Win32 this won’t work with the same commands. Using git-hooks this maybe can be automated.

In the end it’s an practmatic approach and I like it. Thanks prime31 :)