4.2 LFS History Migration, git-fat & File Locking
Key Takeaways
- git lfs migrate import rewrites existing commits so historical binaries become pointers; it changes every commit SHA and therefore requires a coordinated force push.
- After a history rewrite every developer and every build definition must re-clone; pulling into an old clone reintroduces the rewritten objects.
- git-fat is the legacy predecessor to Git LFS, using rsync-style external storage without native Git client integration or file locking.
- Files declared lockable in .gitattributes can be locked with git lfs lock, which makes the file read-only for other users and blocks their push server-side.
- File locking is the standard answer for binary assets that cannot be three-way merged, such as .fbx models, .psd artwork and Office documents.
4.2 LFS History Migration, git-fat & File Locking
Tracking new binaries with Git LFS protects the future; it does nothing about the gigabytes already sitting in history. This section covers the history rewrite that converts an existing repository, the legacy git-fat approach it replaced, and the file locking that stops two people editing the same unmergeable binary.
Migrating Existing Repository History to Git LFS
A critical exam scenario occurs when a team already has a bloated repository where developers committed 10 GB of zip files and installers over the past three years. Simply running git lfs track "*.zip" only affects future commits. It does nothing to remove historical binary blobs embedded in past commit trees.
To remediate historical repository bloat, you must rewrite history using git lfs migrate.
Historical Repository Migration Workflow:
Bloated History: Commit 1 (1GB .zip) ──► Commit 2 (edit) ──► Commit 3 (tip)
│
▼ git lfs migrate import --everything
Cleaned History: Commit 1' (Pointer) ──► Commit 2' (Pointer) ──► Commit 3' (Tip)
(Commit SHAs rewritten across all branches and tags)
Step 1: Analyze Repository Bloat
Before modifying history, run info mode to inspect file sizes across branches:
# Analyze which file extensions consume the most history space
git lfs migrate info --everything
Sample output:
Listing sizes for cache: .git/objects/pack
*.zip 14.2 GB 120/120 files(s) 100%
*.onnx 5.8 GB 12/12 files(s) 100%
*.dll 1.1 GB 340/340 files(s) 100%
Step 2: Execute the History Rewrite
Run git lfs migrate import specifying the target file extensions and including all branches:
# Rewrite all branches, replacing past binary blobs with LFS pointers
git lfs migrate import --include="*.zip,*.onnx,*.dll" --everything
What happens during migrate import:
- Git LFS walks every commit on every local branch and tag.
- When it encounters a file matching
--include, it extracts the binary, computes its SHA-256 hash, stores it as an LFS object, and substitutes a pointer file in that historical commit's tree. - Because tree contents change, every downstream commit SHA is recalculated and rewritten.
.gitattributesis automatically updated and committed to track those patterns going forward.
Step 3: Force-Push Rewritten History & Coordinate Team Action
Because commit SHAs were rewritten, pushing to Azure Repos requires administrative force-push privileges:
# Force-push all rewritten branches and tags
git push origin --force --all
git push origin --force --tags
[!CAUTION] Post-Migration Team Coordination: Rewriting history is a disruptive operation. All team members must be instructed to stash or commit ongoing work, delete their old local clones, and re-clone fresh from origin. If any developer attempts to run
git pullfrom their existing local clone, Git will attempt a 3-way merge between the old history and rewritten history, resurrecting all the deleted binary blobs back into the remote repository!
Evolution: Git LFS vs. Legacy git-fat
Prior to the widespread adoption of Git LFS, early engineering teams used legacy open-source tools such as git-fat or git-annex to manage large files. Understanding the architectural divergence highlights why Git LFS is the universal enterprise standard tested on the AZ-400.
| Architectural Attribute | Legacy git-fat | Modern Git LFS (AZ-400 Standard) |
|---|---|---|
| Underlying Mechanism | Python script invoking rsync/rsync-over-ssh or S3 CLI | Compiled Go binary native extension using Git filter protocol |
| Authentication Model | Separate SSH keys or hardcoded cloud credentials | Integrated with Git Credential Manager (GCM), Microsoft Entra ID, and PATs |
| Platform Integration | None (pure client-side script; server sees unknown text) | Native Server Support in Azure Repos, GitHub, GitLab, and Bitbucket |
| File Locking Support | No file locking mechanism available | Native Server-Enforced File Locking (git lfs lock) |
| Download Granularity | Monolithic sync (fetches all files in manifest) | Selective / Lazy Checkout (downloads only what is in active tree) |
| CI/CD Pipeline Support | Requires custom Python scripts and rsync agents | Native Azure Pipelines task (checkout: self, lfs: true) |
| Exam Recommendation | Obsolete / Legacy Anti-Pattern | Official Microsoft Enterprise Standard |
In modern enterprise environments, git-fat is considered technical debt. Git LFS provides standardized REST APIs, seamless credential pass-through, and granular Azure DevOps service integration.
Preventing Merge Conflicts: Git LFS File Locking
In standard software engineering, two developers can edit different functions within the same Program.cs text file simultaneously. When they merge, Git performs a 3-way text merge and combines the changes cleanly.
For binary files, a 3-way merge is mathematically impossible. Git cannot diff two versions of a Photoshop document (.psd), an Unreal Engine level (.umap), or an audio track (.wav). If two developers push conflicting binary edits, one developer's work must be completely discarded, leading to lost engineering hours.
Git LFS solves this through File Locking.
Git LFS File Locking Workflow:
Developer Alice Azure Repos Server Developer Bob
│ │ │
├─── git lfs lock design.psd ───────────────────►│ (Server marks design.psd locked) │
│ (Lock acquired; local file set read-write) │ │
│ │◄── git lfs lock design.psd ───────────┤
│ ├─── "Locked by Alice" (HTTP 423) ─────►│
│ │ (Lock rejected; file stays RO) │
├─── git commit & git push ─────────────────────►│ │
├─── git lfs unlock design.psd ─────────────────►│ (Lock released) │
Step 1: Declare Files as Lockable
Configure .gitattributes to mark specific binary extensions as --lockable:
git lfs track "*.psd" --lockable
git lfs track "*.fbx" --lockable
git add .gitattributes && git commit -m "chore: set 3D and design assets as lockable"
git push origin main
When a file is designated --lockable, Git LFS automatically sets the file permissions to read-only in the local filesystem upon git checkout. This acts as an immediate physical safeguard, preventing developers from modifying the file until they acquire a server lock.
Step 2: The Locking Workflow
# 1. Acquire an exclusive lock on the remote server
git lfs lock art/textures/hero.psd
# Output: Locked art/textures/hero.psd
# (Local file is automatically converted from read-only to read-write)
# 2. Inspect active locks across the team
git lfs locks
# Output:
# art/textures/hero.psd Alice ID: 104
# 3. Modify, commit, and push the asset
git add art/textures/hero.psd
git commit -m "feat(art): update hero texture resolution"
git push origin main
# 4. Release the lock
git lfs unlock art/textures/hero.psd
# Output: Unlocked art/textures/hero.psd
Server-Side Push Protection
If Developer Bob attempts to push changes to art/textures/hero.psd while Alice holds the active lock, the Azure Repos or GitHub server rejects the push at the pre-receive hook level, preventing corrupted or conflicting binary versions from entering the branch.
In administrative emergencies (e.g., Alice goes on vacation while holding a lock), an authorized repository administrator can force unlock the asset:
# Administrative force-unlock using the lock ID
git lfs unlock --id=104 --force
Git LFS CLI Quick Reference
| Command | Purpose | Operational Context |
|---|---|---|
git lfs install | Installs global LFS filter drivers and hooks | Run once per workstation/agent |
git lfs track "<pattern>" | Tracks file patterns via .gitattributes | Run before adding binary files |
git lfs track "<pattern>" --lockable | Configures file pattern for exclusive locking | Design/gaming/model assets |
git lfs ls-files | Lists all files managed by Git LFS in the commit | Verification & troubleshooting |
git lfs status | Shows staged LFS objects vs Git blobs | Pre-commit validation |
git lfs migrate info --everything | Analyzes historical repository bloat by file type | Pre-migration planning |
git lfs migrate import --everything | Rewrites Git history to convert past blobs to pointers | Post-bloat remediation |
git lfs lock <path> | Acquires exclusive server lock on a file | Prevents binary merge conflicts |
git lfs unlock <path> | Releases active lock | Post-merge cleanup |
git lfs locks | Queries server for all active repository locks | Team coordination |
git lfs prune | Deletes old local cached LFS objects from .git/lfs | Local disk space cleanup |
git lfs pull | Explicitly downloads LFS payloads for current checkout | Fast CI build optimization |
Practical AZ-400 Exam Scenarios & Traps
Scenario 1: The CI/CD Clone Timeout
- Scenario: A gaming company hosts a Unity repository in Azure Repos. The repository has grown to 35 GB. Azure Pipelines CI builds consistently fail with timeouts during the
git clonecheckout task. Developers have added.mp4and.psdfiles to.gitattributesusinggit lfs track, but clone times did not improve. - Root Cause: Tracking files with
git lfs trackonly intercepts new commits. The historical commits still contain 34 GB of binary blobs. - AZ-400 Solution: Execute
git lfs migrate import --include="*.mp4,*.psd" --everythingon an administrative workstation. Force-push the rewritten branches and tags withgit push origin --force --all && git push origin --force --tags. Configure the Azure Pipelines YAML checkout step withlfs: falseif build agents only require source code, orlfs: trueif binaries are needed for compilation.
Scenario 2: Unintentional Binary Commits by Collaborators
- Scenario: A team lead initializes Git LFS on their local laptop, tracks
*.onnxfiles, and pushes a new machine learning model. A junior engineer pulls the repository, adds a new modelv2.onnx, and pushes it. The remote repository size expands by 1.2 GB, and the new model is not stored in LFS. - Root Cause: The team lead ran
git lfs track "*.onnx"but did not commit and push the updated.gitattributesfile to the remote repository. Without.gitattributes, the junior engineer's Git client treatedv2.onnxas a standard Git blob. - AZ-400 Solution: Always commit
.gitattributesinto version control in the same commit where LFS tracking is configured. Implement server-side push validation or branch policies to reject commits containing binary blobs larger than a specified threshold (e.g., 50 MB).
A development team's Azure Repos Git repository has expanded to over 28 GB due to large archive files (.zip) committed over several years. A developer runs 'git lfs track "*.zip"' and commits the .gitattributes file, but fresh clones on ephemeral Azure Pipelines build agents still take 20 minutes to download the 28 GB packfile. What action must the DevOps engineer take to permanently resolve the cloning delay?
A game development studio using Azure Repos encounters frequent file corruption and lost work because multiple 3D animators push concurrent, conflicting modifications to the same binary character model file (character.fbx). Git cannot perform a 3-way merge on binary files. How should the DevOps engineer configure the repository to systematically prevent this problem?