The Titan's Forges
Git LFS, binary files and video games
Beyond the Citadel, in the most remote mountains of the realm, stand the Titan's Forges - gigantic workshops carved into volcanic rock. Here, artisans work on artifacts so massive that no ordinary scroll could contain them: enchanted armor, maps of entire worlds, three-dimensional models of cathedrals.
The Master Blacksmith greets you with a grunt. His hands are covered in soot and runes.
"Standard chronicles are made for text - words, incantations, formulas. But here, we work with massive artifacts. Textures, 3D models, sounds, animations. If you try to archive them like ordinary text, your chronicles will explode in size and become unusable. You need a special tool. You need the Titan's Forges."
Why binary files break Git
To understand the problem, you first need to understand how Git stores files internally.
Delta compression
When you modify a text file, Git doesn't store a complete copy at each commit. It calculates the difference (the delta) between the old and new versions. If you change 3 lines in a 1000-line file, Git only stores those 3 modified lines.
With a binary file (a PSD image, a 3D model, an audio file), everything changes with each modification. Even a minor change - moving a layer by 1 pixel - can modify the entire binary file. Git cannot calculate an efficient delta. Result: each version is stored in full.
Pack files
Git periodically groups objects into pack files to save space. This compression works very well for text, but is nearly useless for already-compressed binaries (PNG, ZIP, PSD, etc.).
The exploding history
Imagine a video game project:
- You have a 50 MB texture
- You modify it 10 times during development
- Git stores 10 near-complete copies = 500 MB for a single file
- Multiply by hundreds of textures, models, sounds...
- Your repository reaches several dozen GB in a few months
And the worst part: a git clone downloads the entire history. A new developer joining the project must download dozens of GB before they can start working.
Git LFS - Large File Storage
Git LFS (Large File Storage) is an official Git extension that solves this problem. The principle is simple: instead of storing large files in the Git repository, LFS replaces them with small pointer files and stores the real files on a separate server.
# Without LFS: the binary file is IN the Git repo
texture.psd (50 MB) -> stored in .git/objects/
# With LFS: a lightweight pointer in Git, the real file elsewhere
texture.psd (134 bytes, pointer) -> stored in .git/objects/
texture.psd (50 MB, real file) -> stored on the LFS server Installation
Bash (Linux / macOS / Git Bash on Windows):
# Linux (Debian/Ubuntu)
sudo apt install git-lfs
# Linux (Fedora)
sudo dnf install git-lfs
# macOS
brew install git-lfs
# Initialize LFS for your user (once only)
git lfs install PowerShell (Windows):
# Windows (with winget)
winget install GitHub.GitLFS
# Or download from https://git-lfs.com
# Initialize LFS for your user (once only)
git lfs install The git lfs install command configures the necessary Git hooks in your global configuration. You only run it once per machine.
Tracking files
To tell LFS which files to manage, you use git lfs track:
# Track all PSD images
git lfs track "*.psd"
# Track multiple file types
git lfs track "*.png"
git lfs track "*.jpg"
git lfs track "*.wav"
git lfs track "*.mp3"
git lfs track "*.fbx"
git lfs track "*.blend"
# Track a specific file
git lfs track "assets/huge-map.tga" Each call to git lfs track adds a line to the .gitattributes file at the root of your project:
# Contents of .gitattributes after the commands above
*.psd filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
assets/huge-map.tga filter=lfs diff=lfs merge=lfs -text Important: the .gitattributes file must be committed to the repository! It tells other developers which files are managed by LFS. Commit it first, before adding any binary files.
Daily operations
Once LFS is configured, you use Git normally. The git add, git commit, git push and git pull commands work the same way - LFS handles everything behind the scenes.
# Add and commit an LFS file - nothing changes!
git add assets/texture.psd
git commit -m "Add hero texture"
git push
# See files managed by LFS
git lfs ls-files
# Pull LFS files from the server
git lfs pull
# Push LFS files to the server
git lfs push --all origin
# Fetch LFS files without checkout
git lfs fetch Check LFS status
# See which patterns are tracked
git lfs track
# See LFS files in the repository
git lfs ls-files
# See detailed status
git lfs status
# See LFS environment information
git lfs env File Locking - locking binary files
With source code, if two developers modify the same file, Git can often merge the changes automatically. With a binary file, that's impossible. You can't "merge" two versions of a PSD file or a 3D model.
The solution: file locking. Before modifying a binary file, you lock it. Nobody else can modify it until you unlock it.
# Lock a file before modifying it
git lfs lock assets/main-character.blend
# See locked files (yours and others')
git lfs locks
# Unlock when you're done
git lfs unlock assets/main-character.blend
# Force unlock (if someone forgot)
git lfs unlock assets/main-character.blend --force Best practice: in a team, establish this rule: never modify a binary file without locking it first. This prevents impossible-to-resolve conflicts and hours of lost work.
To make locking mandatory on certain files, add the lockable attribute in .gitattributes:
# Make locking mandatory for Blender files
*.blend filter=lfs diff=lfs merge=lfs -text lockable With lockable, files are automatically set to read-only as long as they are not locked. This prevents accidental modifications.
Integration with game engines
Each game engine has its own specifics. Here's how to configure Git + LFS for the three most popular engines.
Unity
Essential configuration:
- Asset Serialization: in Edit > Project Settings > Editor, set Asset Serialization Mode to Force Text. This allows Git to diff scene and prefab files.
- Meta files: enable Visible Meta Files. The
.metafiles must be committed - they contain the GUIDs that link assets together. (Note: since Unity 2020+, this setting is the default behavior and has been removed from the interface. Meta files are always visible.)
.gitignore for Unity:
# .gitignore Unity
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
# IDE
.vs/
.vscode/
*.csproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
# Builds
*.apk
*.aab
*.unitypackage
*.app
# Crashlytics
crashlytics-build.properties Recommended LFS patterns for Unity:
# Textures and images
git lfs track "*.psd"
git lfs track "*.png"
git lfs track "*.jpg"
git lfs track "*.tga"
git lfs track "*.tif"
git lfs track "*.exr"
git lfs track "*.hdr"
# Audio
git lfs track "*.wav"
git lfs track "*.mp3"
git lfs track "*.ogg"
# 3D Models
git lfs track "*.fbx"
git lfs track "*.obj"
git lfs track "*.blend"
# Video
git lfs track "*.mp4"
git lfs track "*.mov"
# Fonts
git lfs track "*.ttf"
git lfs track "*.otf"
# Unity specific - large files
git lfs track "*.unitypackage"
# Note: *.asset is YAML text if Force Text is active.
# LFS is optional for these files - only useful
# if they are very large despite the text format. Warning: do NOT put .unity (scenes) or .prefab files in LFS if you have Force Text enabled. These are readable YAML files that Git can diff and merge. Only real binaries go into LFS.
Unreal Engine
Unreal uses proprietary binary formats (.uasset, .umap) for nearly everything. This is a case where LFS is absolutely essential.
.gitignore for Unreal Engine 5:
# .gitignore Unreal Engine 5
/Binaries/
/DerivedDataCache/
/Intermediate/
/Saved/
/.vs/
/.vscode/
# Build
/Build/
# Compiled
*.VC.db
*.opensdf
*.opendb
*.sdf
*.suo
*.sln.docstates
# Temporary files
*.tmp
*.log LFS patterns for Unreal:
# Unreal Assets (ALL binary)
git lfs track "*.uasset"
git lfs track "*.umap"
# Textures and media (same patterns as Unity)
git lfs track "*.png"
git lfs track "*.jpg"
git lfs track "*.psd"
git lfs track "*.tga"
git lfs track "*.exr"
git lfs track "*.hdr"
git lfs track "*.wav"
git lfs track "*.mp3"
git lfs track "*.mp4"
git lfs track "*.fbx"
git lfs track "*.blend" Godot
Godot is the most Git-friendly of the three engines. The majority of its formats are text-based:
.tscn(scenes): text format - Git can diff and merge.tres(resources): text format.gd(GDScript scripts): pure text.gdshader(shaders): pure text
Only imported assets (images, sounds, models) are binary.
.gitignore for Godot:
# .gitignore Godot
/.godot/ # Godot 4 (cache, imports, editor data)
/android/
/export/
*.translation
# Import cache Godot 3 (regenerated automatically)
# In Godot 4, this folder is replaced by .godot/
.import/ LFS patterns for Godot:
# Only real binaries need LFS with Godot
# NOT *.tscn or *.tres: these are text formats (S-expression)
# that Git can diff and merge normally
git lfs track "*.png"
git lfs track "*.jpg"
git lfs track "*.wav"
git lfs track "*.ogg"
git lfs track "*.mp3"
git lfs track "*.glb" # binary glTF - goes into LFS
# Note: *.gltf is JSON text, only *.glb (binary version) goes into LFS
git lfs track "*.fbx"
git lfs track "*.blend"
git lfs track "*.ttf"
git lfs track "*.otf"
# Note: *.svg is XML text, no need for LFS Godot advantage: thanks to its native text formats, you need much less LFS than with Unity or Unreal. Scenes and resources can be diffed and merged normally with Git. This is a real asset for version control.
Migrating an existing project to LFS
You have an existing project with binary files already committed in the history? Git LFS provides a migration tool to rewrite the history and move files to LFS retroactively.
Warning: migration rewrites Git history. This means all team members will need to re-clone the repository after migration. Warn everyone before migrating.
# See which files would benefit from LFS (history analysis)
git lfs migrate info --everything
# See the largest files
git lfs migrate info --everything --top=20
# Migrate all PSD files across the entire history to LFS
git lfs migrate import --everything --include="*.psd"
# Migrate multiple file types in a single command
git lfs migrate import --everything --include="*.psd,*.png,*.wav,*.fbx"
# Migrate only the current branch
git lfs migrate import --include="*.psd"
# After migration, push the new history (force push required!)
git push --force-with-lease You can also do the reverse operation - remove files from LFS:
# Export files from LFS back to normal Git storage
git lfs migrate export --everything --include="*.svg" Alternatives to Git LFS
Git LFS isn't the only solution for managing large files. Here are two notable alternatives:
git-annex - flexible storage
git-annex is an older and more flexible tool than Git LFS. Instead of storing files on a centralized LFS server, git-annex can store them anywhere: external hard drive, SSH server, S3, Backblaze, etc.
# Initialize git-annex in a repository
git annex init "my-workstation"
# Add a file to git-annex
git annex add assets/large-file.blend
# Add a storage remote (external drive)
git annex initremote external-drive type=directory directory=/media/backup/annex
# Copy files to the remote
git annex copy --to external-drive
# Retrieve a file
git annex get assets/large-file.blend Advantages: decentralized storage, no LFS server needed, fine-grained control over where each file is stored. Disadvantages: more complex than LFS, less integrated with forges (GitHub, GitLab).
DVC - Data Version Control
DVC is designed for data science and machine learning (we'll cover it in detail in Quest A2). It manages large data files and ML models with a system of reproducible pipelines.
# Add a large dataset to DVC (instead of Git)
dvc add data/dataset.csv
# The file is replaced by a .dvc pointer
# data/dataset.csv.dvc -> committed in Git
# data/dataset.csv -> stored by DVC (local or remote) Advantages: reproducible pipelines, integrated with the ML ecosystem. Disadvantages: not designed for video games or design.
| Criterion | Git LFS | git-annex | DVC |
|---|---|---|---|
| Use case | Video games, design, binaries | Flexible multi-backend storage | Data science, ML |
| Forge integration | Excellent (GitHub, GitLab) | Limited | Good |
| Complexity | Low | Medium to high | Medium |
| File locking | Yes | No | No |
| Storage | LFS server (forge) | Anywhere | S3, GCS, local, SSH |
Best practices for a game repository
Organizing a video game repository requires discipline. Here are the rules that have proven themselves in the industry:
Recommended structure
my-game/
βββ .gitignore # Exclude generated files
βββ .gitattributes # LFS configuration
βββ README.md
βββ Assets/ # Or the engine's root folder
β βββ Art/
β β βββ Textures/ # -> LFS
β β βββ Models/ # -> LFS
β β βββ Animations/ # -> LFS
β β βββ UI/ # -> LFS (except SVG)
β βββ Audio/
β β βββ Music/ # -> LFS
β β βββ SFX/ # -> LFS
β β βββ Voice/ # -> LFS
β βββ Scripts/ # -> Normal Git (text)
β βββ Scenes/ # -> Depends on the engine
β βββ Prefabs/ # -> Depends on the engine
βββ Docs/ # Documentation (text)
βββ Tools/ # Tool scripts (text) Golden rules
- Configure LFS before the first binary commit. Migrating after the fact is possible but painful.
- Commit
.gitattributesfirst. Before any binary file. - Lock before modifying a binary. Always. No exceptions.
- Use short-lived branches. The longer a branch lives, the more likely binary conflicts become.
- Communicate with your team. "I'm working on the boss model" - a simple announcement can prevent hours of conflicts.
- Prefer text formats when possible. SVG instead of PNG for icons, YAML instead of binary for configs, etc.
- Don't commit generated files. Builds, caches, temporary files have no place in the repository.
Practical exercise - Setting up a game repo
Configure a Git repository with LFS for a fictional video game project. You'll create the structure, configure LFS, and simulate a workflow with binary files.
Step 1 - Create the repository and initialize LFS
# Create the project
mkdir titans-forges-project
cd titans-forges-project
git init -b main
# Initialize LFS
git lfs install # Create the project
mkdir titans-forges-project
cd titans-forges-project
git init -b main
# Initialize LFS
git lfs install Step 2 - Configure files to track
# Track common binary file types
git lfs track "*.png"
git lfs track "*.jpg"
git lfs track "*.psd"
git lfs track "*.wav"
git lfs track "*.mp3"
git lfs track "*.fbx"
git lfs track "*.blend"
# Verify that .gitattributes was created
cat .gitattributes # Track common binary file types
git lfs track "*.png"
git lfs track "*.jpg"
git lfs track "*.psd"
git lfs track "*.wav"
git lfs track "*.mp3"
git lfs track "*.fbx"
git lfs track "*.blend"
# Verify that .gitattributes was created
Get-Content .gitattributes Step 3 - Create the project structure
# Create the directory tree
mkdir -p Assets/Art/Textures
mkdir -p Assets/Art/Models
mkdir -p Assets/Audio/Music
mkdir -p Assets/Audio/SFX
mkdir -p Assets/Scripts
mkdir -p Docs
# Create a .gitignore
cat > .gitignore << 'EOF'
# Builds
/Build/
/Builds/
# IDE
.vs/
.vscode/
*.suo
*.user
# OS
.DS_Store
Thumbs.db
# Temporary
*.tmp
*.log
EOF
# Create a README
echo "# My Game Project - The Titan's Forges" > README.md # Create the directory tree
New-Item -ItemType Directory -Force -Path Assets/Art/Textures
New-Item -ItemType Directory -Force -Path Assets/Art/Models
New-Item -ItemType Directory -Force -Path Assets/Audio/Music
New-Item -ItemType Directory -Force -Path Assets/Audio/SFX
New-Item -ItemType Directory -Force -Path Assets/Scripts
New-Item -ItemType Directory -Force -Path Docs
# Create a .gitignore
@"
# Builds
/Build/
/Builds/
# IDE
.vs/
.vscode/
*.suo
*.user
# OS
.DS_Store
Thumbs.db
# Temporary
*.tmp
*.log
"@ | Set-Content .gitignore
# Create a README
"# My Game Project - The Titan's Forges" | Set-Content README.md Step 4 - First commit (configuration)
# Commit the configuration first
git add .gitattributes .gitignore README.md
git commit -m "Initialize project with LFS and .gitignore" # Commit the configuration first
git add .gitattributes .gitignore README.md
git commit -m "Initialize project with LFS and .gitignore" Step 5 - Simulate binary files
# Create simulated "binary" files
dd if=/dev/urandom of=Assets/Art/Textures/hero.png bs=1024 count=100 2>/dev/null
dd if=/dev/urandom of=Assets/Audio/SFX/sword.wav bs=1024 count=50 2>/dev/null
# Add a script (normal text file, not LFS)
cat > Assets/Scripts/player.gd << 'EOF'
extends CharacterBody2D
var speed = 200.0
func _physics_process(delta):
var velocity = Vector2.ZERO
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
velocity = velocity.normalized() * speed
move_and_slide()
EOF
# Commit
git add .
git commit -m "Add first assets and player script"
# Verify that LFS is managing the binary files
git lfs ls-files # Create simulated "binary" files
$bytes = New-Object byte[] 102400
(New-Object Random).NextBytes($bytes)
[IO.File]::WriteAllBytes("Assets/Art/Textures/hero.png", $bytes)
$bytes = New-Object byte[] 51200
(New-Object Random).NextBytes($bytes)
[IO.File]::WriteAllBytes("Assets/Audio/SFX/sword.wav", $bytes)
# Add a script (normal text file, not LFS)
@"
extends CharacterBody2D
var speed = 200.0
func _physics_process(delta):
var velocity = Vector2.ZERO
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
velocity = velocity.normalized() * speed
move_and_slide()
"@ | Set-Content Assets/Scripts/player.gd
# Commit
git add .
git commit -m "Add first assets and player script"
# Verify that LFS is managing the binary files
git lfs ls-files Step 6 - Verify the result
# LFS files should appear here
git lfs ls-files
# The script should NOT appear (it's text, not LFS)
# You should see hero.png and sword.wav
# See tracked patterns
git lfs track Command summary
| Command | Description |
|---|---|
| git lfs install | Initialize LFS (once per machine) |
| git lfs track "*.ext" | Track a file type with LFS |
| git lfs untrack "*.ext" | Stop tracking a file type |
| git lfs ls-files | List files managed by LFS |
| git lfs status | See LFS file status |
| git lfs pull | Download LFS files |
| git lfs push --all origin | Push all LFS files |
| git lfs fetch | Fetch LFS files without checkout |
| git lfs lock <file> | Lock a binary file |
| git lfs unlock <file> | Unlock a binary file |
| git lfs locks | List locked files |
| git lfs migrate info | Analyze repository for migration |
| git lfs migrate import | Migrate existing files to LFS |
| git lfs env | See LFS configuration |
The Master Blacksmith slowly nods, satisfied.
"You see now why these Forges exist. Massive artifacts - textures, models, sounds - cannot be treated like simple scrolls. They need their own storage system, their own discipline."
"LFS is your blacksmith's hammer. File locking is your discipline. And .gitattributes is your forge blueprint. With these three tools, you can version any project, even the most titanic ones."
He returns to his anvil, the sound of his hammer echoing through the mountains.
"Now go forge your own artifacts. And never forget: commit .gitattributes first. Always."