Using Git for version control in a Unity project requires a small amount of initial setup, but it can all be done quickly using the command-line client. This article covers initializing a new project using Git with Large File Storage (LFS) to handle the substantial number of binary files required by game development.

Note: I’ll be using the Git command-line client (MinTTY) for Windows for the rest of the article.

Configuring a Unity Project for Git

There’s a minimal amount of configuration required when using Git in a Unity project. The following settings are the default in newer versions of Unity, but it’s still worth a quick check to make sure they’re configured correctly.

  • Edit / Project Settings / Editor
  • Version Control
    • Mode: Visible Meta Files
  • Asset Serialization
    • Mode: Force Text

The Version Control setting simply ensures that the .meta files that Unity creates alongside all asset files and directories are not hidden on disk. The command-line Git client will still find these files, but making them visible will ensure that other clients don’t have issues finding them.

The Asset Serialization setting ensures that Unity stores its asset files as text instead of binary. This makes merging changes possible and allows Git to store changes as smaller deltas instead of a full copy of the file to save storage space.

Configuring Git User Settings

Before creating or cloning a repository, it’s worth configuring a few user-specific settings for Git. To set a user name and email that will be used when committing changes:

git config --global user.name "John Doe"
git config --global user.email "[email protected]"

These settings will be added to the per-user configuration file ~/.gitconfig which can be edited directly by running:

git config --global -e

It’s also possible to override these settings per repository by removing the --global option from these commands and setting them once the repository is created. There are several more user-specific settings that are worth reviewing later such as the default editor, diff and merge tools, command aliases, and more.

Configuring Perforce P4Merge for Git

I’m a fan of Perforce’s P4merge for file diffs and interactive merges. To set this up, simply add the following to your ~/.gitconfig file:

[diff]
	tool = p4merge
[difftool]
	prompt = false
[difftool "p4merge"]
	cmd = 'C:/Program Files/Perforce/p4merge.exe' \"$REMOTE\" \"$LOCAL\"
    prompt = false
[merge]
	tool = p4merge
[mergetool]
	keepBackup = false
[mergetool "p4merge"]
	cmd = 'C:/Program Files/Perforce/p4merge.exe' \"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"
	trustExitCode = true

Configuring UnityYAMLMerge for Git

Unity includes a tool ( UnityYAMLMerge) to assist in merging scene and prefab files. Unfortunately, despite most .asset files being YAML, UnityYAMLMerge does not attempt to merge them as well (unless the force option is specified).

To configure UnityYAMLMerge requires adding a new “merge tool” entry and, to make it easier to use, setting Git’s default merge tool to it. This can be done by modifying ~/.gitconfig (replacing the tool assignment from above):

[merge]
    tool = unityyamlmerge
    
[mergetool "unityyamlmerge"]
    trustExitCode = false
    keepBackup = false
    cmd = 'C:/Program Files/Unity/Editor/Data/Tools/UnityYAMLMerge.exe' merge -p \"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"

When pulling or merging new changes results in a conflict, git mergetool can be run to attempt to automatically (better) resolve the conflicts in scenes or prefabs using UnityYAMLMerge. If it fails to resolve a conflict, it will launch a “fallback” tool specified in the file mergespecfile.txt located in the same folder as UnityYAMLMerge. There are several merge tools defined in this file (including P4Merge) and the first one that is found will be used. The original file can can be edited to specify a different fallback tool, but note that it will be overwritten if a new version of Unity is installed.

Ideally Git could use UnityYAMLMerge during its initial conflict detection (during pull or merge commands) to avoid Git displaying a conflict when one may not truly exist. For example, say a property was simply moved around, but its value was unchanged. It’s also possible that Git’s merge algorithm ignores a true conflict, though less likely. To help catch these cases, it is possible to configure a custom “merge driver” and specify file types that should use it in the .gitattributes file discussed later:

[merge "unityyamlmerge"]
     name = UnityYAMLMerge
     driver = 'C:/Program Files/Unity/Editor/Data/Tools/UnityYAMLMerge.exe' merge --force --fallback none %O %B %A %P

Here the force option is specified because Git uses fully random filenames for the different versions of a file being merged and UnityYAMLMerge does not use the “destination” parameter to detect file type. Please note that I am still experimenting with using this driver, and it may not work with other merge tools besides P4Merge, so if you use it and notice a problem please leave a comment below. (Note the describe option can be added to output detected conflicts from UnityYAMLMerge during a pull or merge.)

Finally, note that both the cmd and driver properties require specifying the location of UnityYAMLMerge.exe (or it being in the PATH) which can be a bit of a hassle especially when using Unity Hub as the location of the UnityYAMLMerge executable will change based on the version in use. Unfortunately, there’s not a great solution to this issue other than possibly moving the executable (and all merge*.txt files) to a more stable location, updating it as needed, and having users add the location to their system search path.

Creating a Git Repository

The next step is to initialize a new Git repository for the Unity project:

cd /c/Projects/MyUnityProject
git init .

This will create a new Git repository inside a .git/ subfolder and should produce the following output:

Initialized empty Git repository in C:/Projects/MyUnityProject/.git/

To verify Git initialized the repository properly, the status command can be used to view the status of files in the project:

git status -s

This will produce output similar to:

?? .vs/
?? Assembly-CSharp-Editor.csproj
?? Assembly-CSharp.csproj
?? Assets/
?? Library/
?? Logs/
?? MyUnityProject.sln
?? Packages/
?? ProjectSettings/
?? Temp/
?? obj/

The ?? denotes that the files and directories are currently visible to Git, but are untracked and have not been committed to the repository. Before adding any files to the repository, it’s important to configure Git to skip over certain files and directories that should not be added such as temporary files or assets that Unity can recreate.

Ignoring Files and Directories (.gitignore)

Configuring which files Git should ignore is done by creating a .gitignore file in the project’s root folder and adding any file types or directories to it that should be skipped. The file can get a little big, so view ours here: .gitignore for Unity Projects. Additional entries can be added as needed following the syntax found in the gitignore documentation.

Once the .gitignore file has been saved, checking Git’s status should now show only the files and directories that need to be tracked:

git status -s

Should now show:

?? .gitignore
?? Assets/
?? Packages/
?? ProjectSettings/

Note that the .gitignore file will be added to the repository (in just a bit) and everyone that clones it will ignore the same files.

Initializing Git LFS

A Git repository normally contains the history of all changes made to its files. Since binary files can’t easily be stored based on their delta (differences), adding game assets such as sound and texture files to a repository can result in performance problems over time especially when cloning a copy of a repository.

This issue is what the Git Large File Storage (LFS) extension was built to address. It works by storing small files in the repository (for each configured large file type) that simply point to the actual files outside of the repository. Large files are then transferred as needed when using normal Git commands. The downside to using LFS is that it does require a server that supports it such as GitHub, Azure DevOps, or Gitea. (See Git Hosting for a small review of hosting options).

LFS support is now included in the Git command-line client and many GUI clients, but there are a few steps to set it up in a new repository. First, initialize Git LFS:

git lfs install

This only needs to be done once on a machine and users that clone the repository later should not need to perform this step. If LFS initialized successfully, it should output:

Updated git hooks.
Git LFS initialized.

Configuring Git LFS (.gitattributes)

The next (very important) step is to specify which files should be managed by Git LFS. This can be done using the git lfs track command, but since there are a large number of file types to configure, it’s easier to simply create and edit the .gitattributes file that stores these settings. This file is also used to specify how Git should treat line-endings for file types as well as the method for performing merges and diffs. More info can be found in the gitattributes documentation.

Once again, this file can get pretty big, so click here to view ours: .gitattributes for Unity Projects. I’ll cover a few of the important sections, starting with a few macros to make it a bit cleaner to read and edit:

# Macro for Unity YAML-based asset files.
[attr]unityyaml -text merge=unityyamlmerge diff

# Macro for all binary files that should use Git LFS.
[attr]lfs -text filter=lfs diff=lfs merge=lfs

The unityyaml macro will be used for several types of assets that are created in Unity that use the YAML file format. The -text option prevents Git from automatically converting line endings between platforms to avoid any parsing issues in Unity. The merge=unityyamlmerge option instructs Git to perform merges using the custom unityyamlmerge merge driver that was setup previously in the .gitconfig file. Finally, the diff option at the end instructs Git to always perform text-based diffs.

The lfs macro will be used for all binary asset files (including a few created by Unity). The parameters here instruct Git to manage these files using LFS.

The next major section specifies file handling for all Unity YAML-based assets using the unityyaml macro:

...
# Unity Text Assets
*.meta unityyaml
*.unity unityyaml
*.asset unityyaml
*.prefab unityyaml
...

The next section handles the few binary assets created by Unity using the lfs macro:

...
# Unity Binary Assets
*.cubemap lfs
*.unitypackage lfs
# Note: Unity terrain assets must have "-Terrain" suffix.
*-[Tt]errain.asset -unityyaml lfs
# Note: Unity navmesh assets must have "-NavMesh" suffix.
*-[Nn]av[Mm]esh.asset -unityyaml lfs
...

Here the parameters instruct Git to treat these types of files as binary files that should be managed by Git’s LFS. Note that Unity’s terrain and navmesh asset files use the .asset extension, but are actually binary files. To prevent them from being processed using the *.asset line in the previous section, a more specific pattern needs to be applied and files named to match. For example: Town-Terrain or Town-NavMesh

Ideally, Unity would have used unique file extensions for these as they seem to violate the Asset Serialization Mode option previously set in the project’s settings. For existing projects with these types of assets, it’s important to rename them or adjust the pattern before committing them to the repository.

Finally the remaining sections contain entries for the vast number of other binary file types:

...
# Image
*.jpg lfs
*.jpeg lfs
*.png lfs
...
# Audio
*.mp3 lfs
*.ogg lfs
*.wav lfs
...

Additional entries can be added as needed for new tools, but be sure to do so before adding (staging) new files. Note that if you do make a mistake and commit binary files to the main repository, it is possible to use the migrate command to move binary files to LFS.

It’s also possible to check which attributes are set for a specific file:

git check-attr -a Assets/Town-Outside-NavMesh.asset

This should output the following which shows that it will be stored using LFS:

Assets/Town-Outside-NavMesh.asset: diff: lfs
Assets/Town-Outside-NavMesh.asset: merge: lfs
Assets/Town-Outside-NavMesh.asset: text: unset
Assets/Town-Outside-NavMesh.asset: unityyaml: unset
Assets/Town-Outside-NavMesh.asset: lfs: set
Assets/Town-Outside-NavMesh.asset: filter: lfs

Committing Files to the Repository

With the initial configuration complete, it’s finally time to start committing files to the repository. I like to check in .gitignore and .gitattributes as the first commit. To do that, the files must be first added to Git’s “staging area” using the add command:

git add .gitignore
git add .gitattributes

Now when a status check is made:

git status -s

The output should look like:

A  .gitattributes
A  .gitignore
?? Assets/
?? Packages/
?? ProjectSettings/

The two files now show A denoting they have been added to the staging area. The staging area is just a list of new or modified files that can now be committed as a set of changes using the commit command:

git commit -m "Added .gitignore and .gitattributes"

Which should output something similar to:

[master (root-commit) ebb2157] Added .gitignore and .gitattributes
 2 files changed, 191 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 .gitignore

And if we run git status -s again we’ll no longer see these two files shown as they are no longer new and have not been modified.

Next, it’s time to add all the files that Unity creates when making a new project. This can be done using the following add command with the -A option to stage all the files recursively:

git add -A

It’s likely git will output a few warnings:

warning: LF will be replaced by CRLF in Assets/Scripts/SimpleCameraController.cs.
The file will have its original line endings in your working directory
...

This warning can be safely ignored, and if desired the warning can be turned off using:

git config --global core.safecrlf false

Now that the files have been staged, they can again be committed to the repository:

git commit -m "Initial commit of project assets."

Running git status -s should no longer list any files in its output.

Pushing Changes to a Remote Server

All of the work setting up the Git repository so far has been done locally. It’s finally time to connect the local repository to one on a remote server. To do this a new repository needs to be created on GitHub, Azure DevOps, etc. This process is pretty straight forward usually, but if possible select the option to create an empty repository without ReadMe files, etc. This will make pushing the initial local repository easier.

Once the repository has been created on the remote server, say GitHub, the server will display the URL used to connect to the repository. To setup the remote using HTTPS, the following command can be run from the local repository directory:

git remote add origin https://...remote url...

Now it should now be possible to push the changes to the local repository to the server:

git push origin master

Once that completes, the files and commits should now be visible on the server.

Cloning the Repository

Now that the repository has been set up and pushed to the server, it should be possible for others to clone a copy of it (assuming they have been granted access if it’s a private repository). This is done using the clone command with the remote repository’s URL:

git clone https://...remote url...

This command will create a subdirectory containing the repository and a working copy of all its files.

Making and Getting Changes to the Repository

After modifying and adding files to the repository, it’s a good idea to commit them to avoid losing work and to allow others to work with the changes. This is done the same way as the initial commit using the add and commit commands. Before attempting to push local changes to the server, it’s usually best to get any changes that may have been already pushed by others. This can be done using the pull command:

git pull

If there are any files that need updating, the pull command will retrieve them to the local repository and apply any changes to the working directory. For any files that cannot be automatically merged due to conflicts, Git will run the configured merge tool. Once the conflicts have been resolved, the modified files will need to be restaged and committed before all the changes can be pushed to the server.

At this point, I usually switch to a nice GUI client as it’s usually easier to review the changes that I’ve made before committing them. The command-line client is still useful for advanced Git commands, and I find that it’s a bit quicker for small changes if I’m already using the command-line.

Git Aliases

To make working with the command-line client a bit easier, Git supports user-defined aliases. These aliases are added to the [alias] section in the ~/.gitconfig file:

[alias]
  ...

If an alias begins with an !, Git will run a complete shell command. For example, to create a commit-all alias that will add all committable files to the staging area and then execute the commit command:

[alias]
    # Add all and commit:
    commit-all = "!git add -A;git commit" 

This alias can then be used to commit all changes by running:

git commit-all -m "Another great commit."

For aliases that are simply parameters to the git command, the ! is not needed nor is the actual git command itself. For example, to create an alias that will show the repository’s status using the short output format:

    # Short git status output:
    s = "status -s"

Aliases can make working with the command-line client much nicer as its possible to give meaningful names to complex commands, and these aliases will be displayed along with Git’s built-in commands when using tab-completion (press TAB after typing git <space>).

For a complete list of the aliases we use, see our Helpful Git Aliases page.

Learning More Git

There is an abundance of great information available for Git and definitely a lot more to learn. Hopefully, this article helped remove some of the mystery of using Git for your projects. Just be sure to update the .gitattributes file as needed so that new binary file types are handled properly by LFS!

To learn more about Git, here are some great additional resources:

See Also