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.
Last updated: September 2026

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:

  1. Git LFS walks every commit on every local branch and tag.
  2. 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.
  3. Because tree contents change, every downstream commit SHA is recalculated and rewritten.
  4. .gitattributes is 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 pull from 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 AttributeLegacy git-fatModern Git LFS (AZ-400 Standard)
Underlying MechanismPython script invoking rsync/rsync-over-ssh or S3 CLICompiled Go binary native extension using Git filter protocol
Authentication ModelSeparate SSH keys or hardcoded cloud credentialsIntegrated with Git Credential Manager (GCM), Microsoft Entra ID, and PATs
Platform IntegrationNone (pure client-side script; server sees unknown text)Native Server Support in Azure Repos, GitHub, GitLab, and Bitbucket
File Locking SupportNo file locking mechanism availableNative Server-Enforced File Locking (git lfs lock)
Download GranularityMonolithic sync (fetches all files in manifest)Selective / Lazy Checkout (downloads only what is in active tree)
CI/CD Pipeline SupportRequires custom Python scripts and rsync agentsNative Azure Pipelines task (checkout: self, lfs: true)
Exam RecommendationObsolete / Legacy Anti-PatternOfficial 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

CommandPurposeOperational Context
git lfs installInstalls global LFS filter drivers and hooksRun once per workstation/agent
git lfs track "<pattern>"Tracks file patterns via .gitattributesRun before adding binary files
git lfs track "<pattern>" --lockableConfigures file pattern for exclusive lockingDesign/gaming/model assets
git lfs ls-filesLists all files managed by Git LFS in the commitVerification & troubleshooting
git lfs statusShows staged LFS objects vs Git blobsPre-commit validation
git lfs migrate info --everythingAnalyzes historical repository bloat by file typePre-migration planning
git lfs migrate import --everythingRewrites Git history to convert past blobs to pointersPost-bloat remediation
git lfs lock <path>Acquires exclusive server lock on a filePrevents binary merge conflicts
git lfs unlock <path>Releases active lockPost-merge cleanup
git lfs locksQueries server for all active repository locksTeam coordination
git lfs pruneDeletes old local cached LFS objects from .git/lfsLocal disk space cleanup
git lfs pullExplicitly downloads LFS payloads for current checkoutFast 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 clone checkout task. Developers have added .mp4 and .psd files to .gitattributes using git lfs track, but clone times did not improve.
  • Root Cause: Tracking files with git lfs track only intercepts new commits. The historical commits still contain 34 GB of binary blobs.
  • AZ-400 Solution: Execute git lfs migrate import --include="*.mp4,*.psd" --everything on an administrative workstation. Force-push the rewritten branches and tags with git push origin --force --all && git push origin --force --tags. Configure the Azure Pipelines YAML checkout step with lfs: false if build agents only require source code, or lfs: true if binaries are needed for compilation.

Scenario 2: Unintentional Binary Commits by Collaborators

  • Scenario: A team lead initializes Git LFS on their local laptop, tracks *.onnx files, and pushes a new machine learning model. A junior engineer pulls the repository, adds a new model v2.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 .gitattributes file to the remote repository. Without .gitattributes, the junior engineer's Git client treated v2.onnx as a standard Git blob.
  • AZ-400 Solution: Always commit .gitattributes into 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).
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D