4. Mesh File Revision Tracking with Git LFS
Why Mesh Files Need LFS, Not Plain Git
A solver mesh is a binary artifact. A mid-size CFD volume mesh in CGNS or Nastran BDF form easily reaches 200–800 MB, and every re-mesh produces a completely new binary blob. If you commit those blobs directly, Git stores a full copy per revision inside .git/objects, and the repository grows without bound — clones become slow, and the history is unusable for anyone who only needs the deck. Git LFS solves this by replacing the binary content in the commit with a small text pointer file and storing the actual bytes in a separate LFS object store. The commit history then contains only pointers, so revisions are cheap to diff, tag, and review.
This case walks through the full loop: register a mesh under LFS tracking, commit a baseline, produce a revised mesh, commit it, and then compare the two revisions using both the pointer metadata and the file-level metadata that LFS records.
Repository and LFS Setup
Start from a clean working directory. The commands below assume Git ≥ 2.40 and Git LFS ≥ 3.4; the exact version is not critical, but git lfs track and git lfs ls-files must be available.
mkdir mesh-rev-demo && cd mesh-rev-demo
git init
git lfs install --local
# Declare which paths LFS should intercept
git lfs track "meshes/**/*.cgns"
git lfs track "meshes/**/*.bdf"
# The tracking rules live in .gitattributes and must be committed
git add .gitattributes
git commit -m "chore(lfs): track CGNS and BDF mesh artifacts"
The generated .gitattributes should contain lines of the form meshes/**/*.cgns filter=lfs diff=lfs merge=lfs -text. The -text flag is important: it prevents Git from attempting line-ending normalization on binary content. Verify the rules took effect before adding any mesh:
git check-attr filter -- meshes/wing/rev_a/wing_v1.cgns
# expected: meshes/wing/rev_a/wing_v1.cgns: filter: lfs
git check-attr returns filter: unspecified, the path does not match any pattern in .gitattributes. Fix the glob before committing — once a binary is committed without LFS, it stays in history and requires a history rewrite to remove.Committing the Baseline Mesh
Place the baseline mesh in a revision-named directory so the path itself carries provenance. A practical layout keeps the mesh identity stable and the revision explicit:
meshes/
wing/
rev_a/
wing_v1.cgns # baseline, 412 MB
mesh_manifest.json
The manifest is a small JSON sidecar that records the parameters used to generate the mesh. It is plain text, so it diffs cleanly and gives reviewers context that the binary cannot:
{
"mesh_id": "wing",
"revision": "rev_a",
"generator": "pointwise-18.4",
"cells": 18420000,
"nodes": 19240000,
"y_plus_target": 0.8,
"boundary_layers": 32,
"notes": "baseline RANS mesh, coarse TE resolution"
}
Commit the baseline and tag it so later comparisons have a stable reference:
git add meshes/wing/rev_a/
git commit -m "feat(mesh): add baseline wing mesh rev_a (18.4M cells)"
git tag -a mesh-wing-rev_a -m "Baseline wing mesh, 18.4M cells, y+ target 0.8"
Confirm the binary went to LFS rather than the object database:
git lfs ls-files
# 3f9a1c2e7b * meshes/wing/rev_a/wing_v1.cgns
The leading * means the LFS object is present locally. A - would mean the pointer is committed but the object has not been fetched — a common state after a fresh clone before git lfs pull.
Producing and Committing the Revised Mesh
After refining the trailing edge, the mesher emits wing_v2.cgns at 21.7M cells. Place it under a new revision directory rather than overwriting rev_a, so both revisions remain addressable by path:
meshes/wing/rev_b/
wing_v2.cgns # 487 MB
mesh_manifest.json
Update the manifest to reflect the change, then commit:
{
"mesh_id": "wing",
"revision": "rev_b",
"generator": "pointwise-18.4",
"cells": 21700000,
"nodes": 22680000,
"y_plus_target": 0.8,
"boundary_layers": 36,
"notes": "refined TE, +4 boundary layers, +18% cells"
}
git add meshes/wing/rev_b/
git commit -m "feat(mesh): add wing rev_b, refined TE (+18% cells)"
git tag -a mesh-wing-rev_b -m "Revised wing mesh, 21.7M cells"
Comparing the Two Revisions
There are three complementary comparison layers, and each answers a different question.
1. Pointer diff — did the content change at all? Because LFS replaces the binary with a pointer, git diff between tags shows the pointer's OID and size fields changing. This is the cheapest possible check that a revision is genuinely new content and not a re-commit of the same bytes:
git diff mesh-wing-rev_a mesh-wing-rev_b -- meshes/wing/rev_a/wing_v1.cgns
A pointer file has the form version https://git-lfs.github.com/spec/v1, followed by oid sha256:<64 hex> and size <bytes>. If the OID is identical across two tags, the mesh bytes are identical regardless of filename or directory.
2. Manifest diff — what changed in the generation parameters? The JSON sidecar diffs as readable text, so reviewers see cell count, boundary-layer count, and notes without downloading hundreds of megabytes:
git diff mesh-wing-rev_a mesh-wing-rev_b -- '**/mesh_manifest.json'
3. LFS metadata — how large is each revision on disk and in the store? git lfs ls-files --long reports the OID, size, and path for every tracked object at the current checkout. To compare sizes across tags without checking out both, query the pointer directly:
git show mesh-wing-rev_a:meshes/wing/rev_a/wing_v1.cgns | head -3
git show mesh-wing-rev_b:meshes/wing/rev_b/wing_v2.cgns | head -3
The size line in each pointer gives the exact byte count of the stored blob, which is what actually consumes LFS quota. A useful sanity check is that the pointer size matches the manifest's implied scale — a 21.7M-cell mesh should not report a pointer size smaller than the 18.4M-cell baseline.
Verification and Failure Symptoms
After the two commits, run a short verification pass. Each check maps to a specific failure mode:
| Check | Command | Pass condition | Failure symptom |
|---|---|---|---|
| Tracking rule active | git check-attr filter -- <path> |
returns filter: lfs |
unspecified → binary will enter .git/objects |
| Object in LFS | git lfs ls-files |
* prefix on both meshes |
- prefix → object not fetched; run git lfs pull |
| Distinct revisions | git show <tag>:<path> | grep oid |
different SHA-256 per tag | identical OID → same bytes committed twice |
| Repo stays light | du -sh .git |
kilobytes to low megabytes | hundreds of MB → LFS filter bypassed |
du -sh .git already shows hundreds of megabytes, the mesh was committed before the .gitattributes rule existed. Adding the rule afterwards does not retroactively convert existing blobs. The correction requires git lfs migrate import --include="meshes/**/*.cgns" followed by a force-push, which rewrites every commit hash — coordinate with all collaborators before doing this.One further habit pays off in review: keep the manifest and the mesh in the same commit. A commit that updates wing_v2.cgns but leaves the manifest at rev_a values produces a misleading diff, and reviewers comparing tags will see a cell-count mismatch that has no physical meaning. Treat the pair as an atomic unit, and the pointer diff plus manifest diff together give a complete, low-bandwidth record of what changed between mesh revisions.