Arc 6 Quest A1

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 is designed for text. Large binary files in Git = history that grows uncontrollably, endless clones, and degraded performance.

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:

  1. Asset Serialization: in Edit > Project Settings > Editor, set Asset Serialization Mode to Force Text. This allows Git to diff scene and prefab files.
  2. Meta files: enable Visible Meta Files. The .meta files 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"

With Unreal, nearly everything is binary. File locking is even more critical than with Unity. Use Blueprints and Data Assets with caution - every modification is an entire binary file.

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

  1. Configure LFS before the first binary commit. Migrating after the fact is possible but painful.
  2. Commit .gitattributes first. Before any binary file.
  3. Lock before modifying a binary. Always. No exceptions.
  4. Use short-lived branches. The longer a branch lives, the more likely binary conflicts become.
  5. Communicate with your team. "I'm working on the boss model" - a simple announcement can prevent hours of conflicts.
  6. Prefer text formats when possible. SVG instead of PNG for icons, YAML instead of binary for configs, etc.
  7. 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

If git lfs ls-files shows your binary files and your text script isn't there, congratulations! You've correctly configured a game repository with Git LFS.

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."