This lesson settles three of the course's debts. In lesson 06-05 we said that if the problem is not shared code but heavy binaries, the answer is not submodules but Git LFS. In 08-04 a cryptic line turned up in .gitattributes — *.psd filter=lfs diff=lfs merge=lfs -text — and we promised to explain it. And in 08-06, when analysing why large binaries poison a repository, we said the correct solution was this one.
Here it is, and it arrives at a good moment: the task-manager team is about to take on graphical assets, a demonstration video and the original design files for ui-components. Carla has added an 84 MB mockup-panel.psd and has already modified it three times. The repository, which weighed 4 MB, now weighs 250 MB, and Bruno takes four minutes to clone what used to take two seconds.
We are going to understand why that happens — not why "binaries are bad", but what exactly Git does with them — how LFS solves it, and above all what price it carries, because LFS is neither free nor painless and it has to be adopted knowing where it hurts.
Contents
- Why Git suffers with large binaries
- The arithmetic of the problem in
task-manager - What Git LFS is: the pointer idea
- Installation and first tracking
- How it is reflected in
.gitattributes - The pointer from the inside
- Cloning, fetching and not fetching objects
- Inspection and maintenance commands
- Migrating a repository that already has binaries in its history
- Limitations and warnings you need to know beforehand
- Alternatives to LFS
- When to use LFS and when not to
- Why Git suffers with large binaries
We have to go back to the data model of lesson 01-04, because the problem is not an arbitrary limitation: it is a direct consequence of how Git works.
Every version is a complete blob
Git does not store differences. It stores the complete content of every version of every file, as a blob object identified by the SHA-1 hash of its content. When you modify a file and commit, "what changed" is not stored: a new blob with the entire file is stored.
flowchart TD
C1["commit 1"] --> T1["tree"] --> B1["blob<br/>mockup.psd v1<br/>84 MB"]
C2["commit 2"] --> T2["tree"] --> B2["blob<br/>mockup.psd v2<br/>84 MB"]
C3["commit 3"] --> T3["tree"] --> B3["blob<br/>mockup.psd v3<br/>84 MB"]
B1 -.->|"they all carry on<br/>existing"| B2
B2 -.-> B3
With text files this does not matter, and here is the key: when packing the repository, Git computes deltas between similar objects. A 40 KB app.js file with a hundred versions compresses down to a tiny fraction, because each version is stored as "the previous one plus these changes".
Why the delta does not work with binaries
Three reasons that pile up:
| Reason | What it implies |
|---|---|
| They are already compressed | A .psd, a .png, an .mp4 or a .zip carries its own compression. Compressing again gains almost nothing, and zlib spends time for no return. |
| A small change alters the whole file | Changing one pixel in a compressed format rewrites the entire stream. The two files do not resemble each other byte for byte even if they look alike visually. |
| Git compares bytes, not semantics | Git does not know that two .psd files are the same image with one different layer. It just sees two byte sequences with nothing in common. |
The result: the delta between two versions of a binary is as large as the binary itself. It can be checked (lesson 08-06):
9c4e2f1a8b3d blob 88080384 88052193 12 7a3f9d2e1c5b blob 88080384 88048871 88052205 2e8b1f4a7c9d blob 88080384 88051002 176101076 4f1a9c3e7b2d blob 41273 8104 264152078
The columns are: hash, type, real size, size in the pack and offset. The three .psd blobs take up almost 84 MB each inside the pack: compression has achieved nothing. Compare it with the text blob on the last line, which goes from 41 KB to 8 KB.
And history is forever
This is what turns an annoying problem into a serious one. As we saw in lesson 08-05 with secrets and in 08-06 with size: deleting the file does not remove the objects. A git rm mockup-panel.psd creates a new commit in which the file is absent, but the three 84 MB blobs are still in the database, reachable from the old commits, and everybody who clones will download them.
Getting them out for real requires rewriting the history with git filter-repo, with all the consequences we studied: every hash from the rewritten point onwards changes, and everybody has to re-fetch the repository.
Everybody eats the cost
And this is the decisive argument. A Git repository is distributed: every clone has the complete history. Carla adding 250 MB of history means that:
- Bruno, Ana and Diego download 250 MB when cloning.
- Every CI job that clones downloads 250 MB (and there are many a day).
- Every
worktreeand every new clone takes up that space on disk. git gctakes longer,git clonetakes longer, and the server serves more traffic.
A large file is not a problem for whoever adds it. It is a tax on the whole team, levied forever.
- The arithmetic of the problem in
task-manager
task-managerLet us put concrete numbers on Carla's case, to get an intuition for the order of magnitude:
| Item | Size | Versions | Occupancy in the history |
|---|---|---|---|
app.js, index.html, styles.css, README.md |
~60 KB | ~400 commits | ~2 MB (with deltas) |
mockup-panel.psd |
84 MB | 3 | ~250 MB |
demo.mp4 (planned) |
120 MB | 2 | ~240 MB |
Graphical assets for ui-components |
~15 MB | ~20 | ~300 MB |
The code — what the project genuinely is — takes up 0.25% of the repository. The rest is binaries that hardly anybody needs in their working copy most days.
There is the observation that justifies LFS: most people, most of the time, only need the latest version of those files, or none at all. Ana, who works on the logic in app.js, does not need the three versions of the .psd; she does not even need the latest one. Nor does the CI server that runs the tests.
Git, by design, gives everything to everybody. LFS breaks that "everything to everybody" only for the files you choose.
- What Git LFS is: the pointer idea
Git LFS (Large File Storage) is an extension of Git — it is not part of Git, it is installed separately — built on top of a mechanism Git does ship out of the box: the .gitattributes filters we saw in lesson 08-04.
The whole idea fits in one sentence:
Instead of the large file, what gets versioned in the repository is a small text file saying where the content is. The real content lives in a separate store and is downloaded only when it is needed.
flowchart TD
subgraph disk["Your working copy"]
A["mockup-panel.psd<br/>84 MB, real file"]
end
subgraph repo["Git repository"]
B["mockup-panel.psd<br/>130 bytes: text pointer<br/>oid sha256:4d7a...<br/>size 88080384"]
end
subgraph store["Server's LFS store"]
C["4d7a...<br/>real content, 84 MB"]
end
A -->|"git add: clean filter"| B
B -->|"git checkout: smudge filter"| A
A -->|"git push: uploads the content"| C
C -->|"git checkout / lfs pull"| A
Two independent mechanisms working at the same time:
The clean filter runs when content goes into Git (git add). It receives the 84 MB file, computes its SHA-256 hash, stores the content in LFS's local cache and returns a three-line text to Git. Git versions that text, which is what ends up in the blob.
The smudge filter runs when content comes out of Git (git checkout). It receives the pointer, looks up the content by its hash (in the local cache or by downloading it from the server) and writes the real file to disk.
The result is that you work exactly as you always did. You open the .psd, edit it, git add, git commit, git push. The filters are invisible. What changes is what gets stored in the history: 130 bytes of text instead of 84 MB of binary.
Why this fixes it
| Effect | Reason |
|---|---|
| Git's history stays small | Each version is 130 bytes, and on top of that it is text, which compresses well |
| Cloning is fast | The pointers get downloaded; of the content, only that of the revision you check out |
| Old versions are not downloaded | The LFS server only sends the objects you ask for |
| You can even avoid downloading the current ones | CI that does not need the .psd can skip the smudge |
| Storage can be managed separately | With its own retention and cost policies |
- Installation and first tracking
Installing
LFS is a separate binary, and everybody on the team has to install it:
# Ana, Ubuntu
sudo apt install git-lfs
# Bruno, macOS
brew install git-lfs
# Carla, Windows 11: it comes with the official Git installer,
# or alternatively:
winget install GitHub.GitLFSAnd then, once per machine and per user:
That command does two specific things, and it is worth knowing because it explains a lot of behaviour:
1. It registers the filters in the global configuration:
filter.lfs.clean git-lfs clean -- %f filter.lfs.smudge git-lfs smudge -- %f filter.lfs.process git-lfs filter-process filter.lfs.required true
It is exactly the filter mechanism of lesson 08-04. filter.lfs.required true means that if LFS is not installed, Git fails instead of leaving loose pointers scattered around the disk. It is what you want.
2. It installs hooks in the repository (pre-push, post-checkout, post-commit, post-merge) that take care of uploading and downloading the objects at the right moment. They are hooks (lesson 06-01), with the same properties as always: they live in .git/hooks/ and are not distributed. That is why whoever clones the repository needs to have run git lfs install on their machine.
Declaring which files are tracked
cd ~/projects/task-manager
git lfs track "*.psd"
git lfs track "*.mp4"
git lfs track "*.sketch"
git lfs track "assets/video/**"The quotes are important: without them, the shell would expand *.psd to the existing files and specific paths would be registered instead of the pattern.
Seeing what has been declared:
Listing tracked patterns
*.psd (.gitattributes)
*.mp4 (.gitattributes)
*.sketch (.gitattributes)
assets/video/** (.gitattributes)And stopping tracking a pattern:
Careful: untrack removes the rule for the future. Files already converted into pointers in the history remain pointers; reverting that requires rewriting the history (section 9).
- How it is reflected in
.gitattributes
.gitattributesHere the promise of lesson 08-04 is fulfilled. git lfs track does not store anything in a file of its own: it writes into .gitattributes, the same file you already know.
# ============================================================
# Line endings (resolves GT-190)
# ============================================================
* text=auto
*.sh text eol=lf
*.bat text eol=crlf
# ============================================================
# Git LFS (GT-251)
# ============================================================
*.psd filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.sketch filter=lfs diff=lfs merge=lfs -text
assets/video/** filter=lfs diff=lfs merge=lfs -textLet us break down the four attributes, which is exactly what was left pending:
| Attribute | What it does |
|---|---|
filter=lfs |
Applies the clean and smudge filters registered as filter.lfs.*. This is the core of the mechanism: it turns content into a pointer on the way in, and a pointer into content on the way out. |
diff=lfs |
Uses LFS's diff driver instead of the generic one. That way git diff shows something useful about the file (size, object identifier) rather than the raw pointer. |
merge=lfs |
Uses LFS's merge driver. As we shall see, it does not merge anything: it forces you to choose a side. |
-text |
Switches off the text attribute: it tells Git not to normalise line endings (lesson 08-04). Without this, a * text=auto like the one above could corrupt the content before the filter sees it. |
The -text is the easiest to overlook and the one that causes the hardest damage to diagnose. That is why git lfs track always puts it in.
A very important practical consequence: .gitattributes is a versioned file. When Carla commits and pushes it, the whole team inherits the LFS configuration automatically. Nobody has to be told which files belong to LFS: it is in the repository. That is what makes LFS usable as a team, and it is a good example of the principle from the previous lesson: shared configuration lives in the repository.
What does not travel in the repository is the LFS installation or its hooks. That is why CONTRIBUTING.md (lesson 10-01) has to say so on its first line.
Order matters
A subtle detail: .gitattributes is applied at the moment of the git add. If you add the .psd before declaring the pattern, it gets versioned as an ordinary binary and it is already in the history. That is why the correct sequence is always:
# 1. Declare the pattern
git lfs track "*.psd"
# 2. Commit .gitattributes BEFORE adding the binaries
git add .gitattributes
git commit -m "chore: GT-251 configure Git LFS for the design files"
# 3. Now, and only now, add the files
git add assets/mockup-panel.psd
git commit -m "feat: GT-251 add the task panel mockup"Committing .gitattributes in a separate, earlier commit is not cosmetic: it guarantees that whoever checks out any point in the history has the correct configuration before they run into the files.
- The pointer from the inside
Let us look at exactly what is stored. On disk, the file is a normal one:
But what Git has versioned is something else:
version https://git-lfs.github.com/spec/v1 oid sha256:4d7a9f2e1c8b3a6d5e0f7c2b9a4d1e8f3c6b0a5d2e9f4c7b1a8d3e6f0c5b2a9d size 88080384
One hundred and thirty bytes. That is all there is in Git's history for each version of the .psd. Three lines:
| Line | Meaning |
|---|---|
version |
The version of LFS's pointer format |
oid sha256:... |
The identifier of the content: the SHA-256 hash of the real file |
size |
The size in bytes of the real content |
Notice the conceptual elegance: LFS applies exactly the same principle as Git — content addressing (lesson 01-04) — but one level further down. Content is identified by its hash, it is immutable, and the same content is never stored twice. LFS uses SHA-256 while Git (still) uses SHA-1, something we shall come back to in lesson 10-06.
To see it from further down, with plumbing (lesson 09-06):
One hundred and thirty bytes in the object database. Against 88,080,384. That is the entire lesson.
What git diff shows
Thanks to the diff=lfs attribute:
diff --git a/assets/mockup-panel.psd b/assets/mockup-panel.psd index 3f8a1c9..7b2e4d6 100644 --- a/assets/mockup-panel.psd +++ b/assets/mockup-panel.psd @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d7a9f2e1c8b3a6d5e0f7c2b9a4d1e8f3c6b0a5d2e9f4c7b1a8d3e6f0c5b2a9d -size 88080384 +oid sha256:9e2f7a4c1b8d3e6f0a5c2b9d4e7f1a8c3b6d0e5f2a9c4b7d1e8f3a6c0b5d2e9f +size 88080384
A useful diff as far as it can be: it says the content has changed and the size is the same. It does not say what has changed in the image, because Git cannot know that.
And here it is worth remembering the trick from lesson 08-04: you can define a textualising diff driver for certain binaries, so that git diff shows readable metadata:
It is not a visual difference, but knowing that the image went from 1200×800 to 2400×1600 is already information. The two mechanisms are compatible: LFS manages the storage, textconv improves the display.
- Cloning, fetching and not fetching objects
Here is most of the practical value of LFS, and also its oddities.
A normal clone
Cloning into 'task-manager'... remote: Enumerating objects: 1284, done. Receiving objects: 100% (1284/1284), 2.14 MiB | 8.42 MiB/s, done. Resolving deltas: 100% (612/612), done. Filtering content: 100% (2/2), 196 MiB | 12.3 MiB/s, done.
Two clearly separated phases:
Receiving objects: Git's history. 2.14 MB. It includes the four hundred commits, all the code and every pointer of every version.Filtering content: LFS downloading the real content of the files in the checked-out revision. 196 MB, two files.
The difference with the scenario without LFS: without it, it would have been 250 MB in the first phase (every version of everything) plus the time to resolve useless deltas. With LFS it is 2 MB of history plus only the current content.
Cloning without downloading the content
For anybody who does not need the binaries — the CI that runs the tests, somebody who is only going to touch app.js — there is an environment variable:
This switches off the smudge filter, so the LFS files stay on disk as text pointers:
version https://git-lfs.github.com/spec/v1 oid sha256:4d7a9f2e1c8b3a6d5e0f7c2b9a4d1e8f3c6b0a5d2e9f4c7b1a8d3e6f0c5b2a9d size 88080384
It is a perfectly valid and very useful situation, but you have to know how to recognise it, because it is the source of the most common support incident with LFS: somebody opens the .psd with their image editor and gets an incomprehensible error, without understanding that what they have is a 130-byte text file.
To make it permanent on a specific machine or repository:
git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"
git config --global filter.lfs.process "git-lfs filter-process --skip"Or, more cleanly and more modernly, to clone without content:
git clone --no-checkout https://git.example.com/team/task-manager.git
cd task-manager
git lfs install --local --skip-smudge
git checkout mainFetching the content when you need it
# Download the content of the LFS files of the current revision
git lfs pull
# Only what matches a pattern
git lfs pull --include="assets/mockup-*.psd"
# Exclude videos, which are the heaviest things
git lfs pull --exclude="*.mp4"And the distinction between fetch and pull, which is the same as in Git (lesson 04-04):
# fetch: downloads into LFS's local cache, without writing to the working copy
git lfs fetch
# fetch --all: ALL the versions of ALL the LFS files in the history
git lfs fetch --all
# fetch of a specific reference
git lfs fetch origin v1.3.0
# checkout: writes into the working copy what is already in the cache
git lfs checkout
# pull = fetch + checkout
git lfs pullCareful with git lfs fetch --all: it downloads the content of every version of every LFS file in the entire history. It is exactly what you were avoiding by adopting LFS. It only makes sense in two cases: making a complete backup, or preparing a migration to another server.
Permanent download filters
If on your machine you never want the videos, it is configured once:
git config lfs.fetchexclude "*.mp4,assets/video/**"
git config lfs.fetchinclude "assets/mockup-*.psd"And for CI, where this matters far more because it runs dozens of times a day:
name: Tests
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: false # do not download LFS content
- name: Install dependencies
run: npm ci
- name: Tests
run: npm test
# Only the job that generates the documentation needs the assets
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: true
- name: Generate the documentation with images
run: npm run docsThat lfs: false is the optimisation with the best return in the whole section. If you have fifty CI runs a day and each one saves 196 MB of downloading, that is almost 10 GB of traffic a day and several minutes of accumulated waiting.
- Inspection and maintenance commands
git lfs ls-files: what is in LFS
4d7a9f2e1c * assets/mockup-panel.psd 9e2f7a4c1b * assets/mockup-list.psd 1c8b3a6d5e - assets/video/demo.mp4
The middle column is the most useful:
| Mark | Meaning |
|---|---|
* |
The real content is on disk (smudge applied) |
- |
There is only a pointer; the content has not been downloaded |
Useful variants:
# With size
git lfs ls-files --size
# In a specific revision
git lfs ls-files v1.3.0
# Only the names, to chain with other commands
git lfs ls-files --name-only
# All the LFS objects in the whole history
git lfs ls-files --allgit lfs status: LFS's git status
On branch GT-251-design-assets
Objects to be pushed to origin/GT-251-design-assets:
assets/mockup-panel.psd (84 MB)
Objects to be committed:
assets/mockup-list.psd (LFS: 9e2f7a4)
Objects not staged for commit:
assets/video/demo.mp4 (File: 1c8b3a6)That last line reveals a real problem: (File: ...) instead of (LFS: ...) means that file is not being managed by LFS, probably because it was added before the pattern was declared. It is the first place to look when the repository grows without explanation.
git lfs env: diagnostics
It shows the version, the URL of the LFS endpoint, the path of the local cache and the filter configuration. It is LFS's equivalent of the git config --list --show-origin from lesson 09-06, and the first command to run when something is not working.
git lfs prune: freeing local space
LFS's local cache (.git/lfs/objects/) gradually accumulates content from old revisions:
# See what it would delete, without deleting anything
git lfs prune --dry-run --verbose
# Delete the local objects that are no longer needed
git lfs pruneprune is conservative by design: it only deletes objects that are committed and pushed to the server, and it keeps those of recent revisions. It is controlled with:
# How many days of recent references are kept (7 by default)
git config lfs.pruneoffsetdays 14
# Verify against the server that the object is there before deleting it locally
git config lfs.pruneverifyremotealways trueThat pruneverifyremotealways is worth switching on: it checks against the server that each object is safe before removing it from your disk. It costs a little time and avoids the only scenario in which prune could do harm.
git lfs migrate info: auditing before acting
migrate: Fetching remote refs: ..., done. migrate: Sorting commits: ..., done. migrate: Examining commits: 100% (412/412), done. *.psd 250 MB 3/3 files *.mp4 240 MB 2/2 files *.zip 38 MB 4/4 files
This command modifies nothing: it analyses the history and says which extensions take up how much. It is the compulsory starting point of any migration, and also a good periodic check-up.
- Migrating a repository that already has binaries in its history
This is task-manager's case: the .psd files are already inside. Adopting LFS now makes future files pointers, but the 250 MB of history are still there and everybody is still downloading them.
To get them out you have to rewrite the history. LFS brings its own tool.
The warning, first
git lfs migrate importrewrites the history. The hashes of all the affected commits and all their descendants change. It is the golden rule of lesson 05-01 in its most forceful version, and it demands exactly the same coordination as the secret cleanup of lesson 08-05.
Concrete consequences:
- Every branch and tag containing rewritten commits will point at new objects.
- Anybody with a clone will have a complete divergence (lesson 09-03).
- Open proposals will be based on commits that are no longer in
main. - Commit signatures (08-05) become invalid, because the signed object is no longer the same one.
The complete procedure
Step 0: agree it with the team and choose the moment.
It is not a technical step and it is the most important one. You need a window in which nobody has unpushed work. Ana gives written notice, a time is set and everybody is asked to push and close their proposals.
# Each person checks that they have nothing outstanding
git status
git log --branches --not --remotes --oneline # unpushed local commits
git stash listThat git log --branches --not --remotes is the exact check: it lists the commits that exist on some local branch and on no remote. If it comes out empty, there is nothing to lose.
Step 1: backups.
# A complete mirror clone, kept aside
git clone --mirror https://git.example.com/team/task-manager.git ~/backups/task-manager-before.git
# And a self-contained bundle, just in case (lesson 09-05)
git bundle create ~/backups/task-manager-$(date +%F).bundle --allStep 2: audit.
This is for deciding which patterns to migrate. Migrate by extension, not by specific file: it is more robust and it also captures the files that were renamed along the way.
Step 3: rehearse on a copy.
git clone https://git.example.com/team/task-manager.git /tmp/migration-rehearsal
cd /tmp/migration-rehearsal
git lfs migrate import --everything --include="*.psd,*.mp4,*.zip"migrate: Fetching remote refs: ..., done. migrate: Sorting commits: ..., done. migrate: Rewriting commits: 100% (412/412), done. main 8a1f6c3d4e5b -> 2c9d4f7a1b8e v1.0.0 3f2a1b9c7d4e -> 6b1e8a3c5d0f v1.1.0 7d4e9c2f1a8b -> 9f3c6b0a5d2e migrate: Updating refs: ..., done. migrate: checkout: ..., done.
Notice that the tags get rewritten too. v1.0.0 no longer points at the same commit. It is a consequence of the immutability of the data model: changing anything in the history changes everything that hangs off it.
Step 4: verify the result of the rehearsal.
# How much has it shrunk?
git count-objects -vH
# What is in LFS now?
git lfs ls-files --all | head -20
# Is the content of the latest revision correct?
git lfs pull
ls -lh assets/
# Is the code still identical? Compare the tree with the original
git rev-parse HEAD^{tree}
git -C ~/projects/task-manager rev-parse HEAD^{tree}This last check is the most reassuring and hardly anybody does it. The tree hash of HEAD summarises all the content of the revision. If it matches the original repository's... it will not match, precisely because the .psd files are now pointers. But the ones for the subdirectories that contain no migrated files should match:
If those two hashes are equal, the migration has not touched the code. It is a cryptographic verification, not an impression.
Step 5: do it for real and publish.
cd ~/projects/task-manager
git lfs migrate import --everything --include="*.psd,*.mp4,*.zip"
# Upload the LFS content to the server's store BEFORE the references
git lfs push --all origin
# And now, at last, the rewritten references
git push --force-with-lease --all origin
git push --force-with-lease --tags originOrder matters: first the LFS content, then the references. If you publish the references first, there will be an interval in which somebody can clone a repository whose pointers point at objects that do not yet exist in the store.
--force-with-lease instead of --force, always (lesson 09-03). And if the server has main as a protected branch (07-06), you will have to unprotect it temporarily and protect it again straight afterwards. Write it into the script, because it gets forgotten.
Step 6: everybody re-fetches.
The instruction for the team, identical to the one for the secret cleanup:
# The simplest, safest and most advisable option: clone again
cd ~/projects
mv task-manager task-manager-old
git clone https://git.example.com/team/task-manager.git
cd task-manager
git lfs install
git lfs pullAnd for anybody who had unpushed work on a local branch:
# In the new clone, fetch the branch from the old clone
git remote add old ~/projects/task-manager-old
git fetch old GT-260-my-branch
# Replant only my commits on the new main (lesson 09-03)
git switch -c GT-260-my-branch
git rebase --onto main old/main old/GT-260-my-branchThat rebase --onto is exactly the technique of lesson 09-03: it takes the commits between the old main and the old branch, and replants them on the new main.
Step 7: clean up the server.
Rewriting the history does not free space on the server by itself: the old objects are still there as long as some reference reaches them. You have to ask the server to run its garbage collection, and on many managed platforms that is a support request. Find out before you start, because otherwise the repository will still weigh the same on the server side and the migration will look as though it failed.
The inverse operation
It turns pointers back into normal files. Useful if you decide to abandon LFS. It requires having the objects downloaded, and it rewrites the history in exactly the same way.
- Limitations and warnings you need to know beforehand
LFS solves a real problem, but it introduces others. These are the ones you need to know before adopting it, not afterwards.
- You need an LFS server
LFS does not work without a server. The store is a separate service speaking a protocol of its own over HTTPS. Most platforms offer it; some self-hosted installations require it to be configured explicitly.
Immediate consequence: a repository with LFS stops being self-contained. If you clone a repository with LFS and the LFS server is unavailable, you have the pointers but not the content. This clashes head-on with Git's distributed nature (lesson 01-01), and it is the most serious philosophical argument against LFS: you have introduced a centralised point into a distributed system.
For a genuinely complete backup you need two things:
git bundle create backup.bundle --all # Git's history
git lfs fetch --all # all the LFS content
# and copy .git/lfs/objects/ as well
- Quotas cost money
LFS storage and its bandwidth usually come with paid quotas on the platforms. And there is a trap that surprises a lot of people: bandwidth is consumed on downloading too. CI that clones with lfs: true fifty times a day can exhaust a monthly quota in a few days. Hence the importance of the lfs: false in section 7.
Besides, LFS objects from the history do not delete themselves. If you migrate an 84 MB file and then delete it, the object still takes up quota. Purging the store is an operation specific to each platform, not a Git command.
- Platforms do not behave the same
There are real differences between implementations: how objects are handled in forks, whether file locking is available, how quotas are applied, how orphaned objects are purged, and what happens when transferring a repository between accounts. Do not take for granted that what works on one platform works the same on another, above all when migrating provider: moving a repository with LFS from one server to another requires moving the objects explicitly.
- LFS files do not merge
This is the most important one day to day. When two branches modify the same LFS file, there is always a conflict, and it is not resolved by merging: you have to choose a winning version.
Auto-merging assets/mockup-panel.psd CONFLICT (content): Merge conflict in assets/mockup-panel.psd Automatic merge failed; fix conflicts and then commit the result.
Objects to be committed:
assets/mockup-panel.psd (LFS: 4d7a9f2 -> 9e2f7a4)
Unmerged paths:
assets/mockup-panel.psdAnd the resolution is the same as for any binary (lesson 03-05):
# Keep the version from my branch
git checkout --ours assets/mockup-panel.psd
git add assets/mockup-panel.psd
# Or the one from the other branch
git checkout --theirs assets/mockup-panel.psd
git add assets/mockup-panel.psd
# Or, if the content genuinely has to be combined:
# open both versions in the design application and redo the work by hand
git show :2:assets/mockup-panel.psd > /tmp/mine.psd
git show :3:assets/mockup-panel.psd > /tmp/theirs.psdThose :2: and :3: are the index stages we saw in lesson 03-05 and in 09-06: 1 is the common base, 2 is "ours", 3 is "theirs". Extracting them lets you open them in the corresponding application and decide with the information in front of you.
The organisational consequence is that with binaries you cannot work in parallel. The real solution is not technical but a matter of coordination: only one person should touch each design file at a time. LFS offers a mechanism for formalising that, file locking:
# Declare that a pattern requires locking (in .gitattributes)
git lfs track "*.psd" --lockable
# Lock before editing
git lfs lock assets/mockup-panel.psd
# See what is locked and by whom
git lfs locks
# Release when finished
git lfs unlock assets/mockup-panel.psdWith --lockable, LFS marks those files as read-only on disk until you lock them. It is a mutual exclusion mechanism, as in the centralised systems (lesson 01-01), and it is proof that for certain workflows that model made sense. It requires server support.
- The whole team has to install it
If somebody clones without LFS installed and filter.lfs.required is not active, they will get text pointers instead of files, and — worst case — they could commit a real file on top of a pointer, breaking consistency. With git lfs install done properly this does not happen, but it is the reason the installation has to be the first thing in CONTRIBUTING.md.
- Some operations become slower or stranger
git checkoutbetween branches with different LFS files implies downloads.git bisect(06-02) over a history with LFS may download content at every step.worktrees (06-06) share the LFS cache, which is good, but each one needs its owncheckout.- The file-by-file
smudgecan be slow;git lfs pullis more efficient because it downloads in batches.
- Alternatives to LFS
LFS is not the only answer, and sometimes it is not the best one.
| Option | How it works | For | Against | When to choose it |
|---|---|---|---|---|
| Git LFS | Versioned pointer + separate store, via .gitattributes filters |
Integrated into the normal workflow; supported by almost every platform; genuinely versions the binary | Requires a server; paid quotas; does not merge; breaks the clone's autonomy | Binaries that change and that form part of the product: designs, assets, models |
git annex |
Replaces files with symbolic links to a content store; supports many backends (disks, S3, another computer) | Very flexible; works without a dedicated server; can spread copies over several stores; excellent for enormous archives | Much steeper learning curve; less platform support; a mental model of its own | Scientific archives, large collections, environments with heterogeneous or offline storage |
| Not versioning the binary | The file lives outside (object storage, a shared drive, an asset manager) and the repository stores only a reference (URL + hash) | No cost or complexity in Git; no LFS quotas; each system does its own job | The coordination has to be built; the binary is not versioned with the code; risk of the reference breaking | Large assets that change little: marketing videos, datasets, precompiled dependencies |
| Rethinking the workflow | Version the source and generate the binary; or keep the original where it was created and version only the lightweight export | Eliminates the problem at the root; usually improves the process | Not always possible | When the binary is derived from something smaller |
The fourth deserves emphasis
Before installing anything, ask yourself three questions:
-
Is this file generated? A distribution
.zip, a.pngexported from an.svg, a compiled executable. If it can be regenerated, it should not be in the repository (lesson 08-03): it goes into.gitignoreand CI produces it as an artefact. -
Do I need its history? A demonstration video that is replaced wholesale every six months gains nothing from being versioned. A
.psdthat is iterated on daily and that you sometimes need to go back to last week's version of, does. -
Can I version the source instead of the result? An
.svgis text: it versions, diffs and merges perfectly. A 4 MB.pngexported from that.svgcontributes nothing. Changing the working format solves the problem better than any tool.
Applied to task-manager:
| File | Decision | Reason |
|---|---|---|
mockup-panel.psd (84 MB, changes often) |
LFS | It is iterated on, the history is needed and it is a source, not a derivative |
demo.mp4 (120 MB, redone wholesale) |
Outside the repository | Its history is not needed; a URL and a hash are enough |
logo.svg |
Ordinary Git | It is text: it diffs and it merges |
logo-512.png (generated from the .svg) |
.gitignore + generate in CI |
It is derived |
ui-components assets |
LFS in the submodule | Each repository decides for itself (lesson 06-05) |
- When to use LFS and when not to
Use it when all of these hold
- You have binary files of more than a few MB.
- They change fairly often, generating many versions.
- You need their history: you have to be able to go back to a previous version.
- They are not derived from anything smaller that you could version instead.
- You have an LFS server available and a budget for its quota.
- Everybody on the team can install it.
Do not use it when
- The files are small (less than 1 MB): the pointer and the complexity are not worth it.
- They are generated: they go into
.gitignore. - You do not need the history: keep them outside with a reference.
- They are text, even if large: Git handles them well with deltas and merges them.
- You do not have an LFS server and you are not going to have one.
- The repository has to work completely offline from a single clone.
The decision rule
flowchart TD
A["I have a large file"] --> B{"Is it text?"}
B -->|yes| C["Ordinary Git.<br/>The deltas work well"]
B -->|no| D{"Is it generated?"}
D -->|yes| E[".gitignore + CI artefact"]
D -->|no| F{"Do I need<br/>its history?"}
F -->|no| G["Outside the repository:<br/>URL + verification hash"]
F -->|yes| H{"Can I version<br/>the source instead?"}
H -->|yes| I["Version the source.<br/>Generate the rest"]
H -->|no| J{"Do I have an LFS<br/>server and quota?"}
J -->|yes| K["Git LFS"]
J -->|no| L["git annex or<br/>an external store"]
The most frequent answer in a project like task-manager is "you do not need it". A pure code repository with a handful of icons and a logo has no problem that LFS solves. The need appears when design files, video, audio, 3D models, datasets or game assets come in. In a repository of 50,000 files with graphical assets, LFS is indispensable; in one of four files, it is complexity with no benefit.
Common Mistakes and Tips
Mistake 1: adopting LFS after having put the binaries in. Declaring the pattern only affects the future. The objects that are already in the history are still there and are still being downloaded. git lfs migrate import and a coordinated rewrite are needed. Configure LFS the day you create the repository, not when it weighs 2 GB.
Mistake 2: forgetting the -text. Without it, a * text=auto can normalise line endings in a binary and corrupt it. git lfs track puts it in; if you edit .gitattributes by hand, do not take it out.
Mistake 3: committing the binaries before .gitattributes. The filters are applied at the moment of the add. .gitattributes first, in its own commit, and the binaries afterwards.
Mistake 4: not realising that you have pointers. If an image file weighs 130 bytes and starts with version https://git-lfs..., it is a pointer. Run git lfs pull. Quick check:
Mistake 5: leaving CI downloading LFS when it does not need it. It is the fastest route to exhausting the bandwidth quota. lfs: false except in the jobs that genuinely need the files.
Mistake 6: git lfs fetch --all out of habit. It downloads the entire history of content, which is exactly what LFS exists to avoid. Only for backups or migrations.
Mistake 7: believing a clone with LFS is a complete backup. It is not: you are missing the LFS objects you have not downloaded. A real backup needs the bundle and the LFS content.
Mistake 8: two people editing the same .psd at once. It will end in an unresolvable conflict and somebody will lose work. Use --lockable and git lfs lock, or simply agree who touches what.
Tip 1: audit before deciding. git lfs migrate info --everything --above=1Mb tells you exactly what is inflating the repository, without touching anything.
Tip 2: put a size limit in CI. An automatic guard prevents the problem at the root (lesson 07-06):
- name: Reject large files outside LFS
run: |
LIMIT=$((5 * 1024 * 1024))
LARGE=$(git diff --name-only --diff-filter=ACM origin/main...HEAD | while read -r f; do
[ -f "$f" ] || continue
# LFS pointers are tiny, so they never trigger the warning
SIZE=$(wc -c < "$f")
[ "$SIZE" -gt "$LIMIT" ] && echo "$f ($SIZE bytes)"
done)
if [ -n "$LARGE" ]; then
echo "Files over 5 MB outside LFS:"
echo "$LARGE"
echo "Declare the pattern with 'git lfs track' or take them out of the repository."
exit 1
fiTip 3: document LFS in CONTRIBUTING.md. Three lines at the top: install git-lfs, run git lfs install, and if you see 130-byte files, run git lfs pull. It saves half the support questions.
Tip 4: test the migration on a disposable clone. Always. And verify by comparing the tree hash of the code directories before and after: if they match, the code is intact.
Tip 5: git lfs prune from time to time. The local cache grows without limit. With lfs.pruneverifyremotealways = true it is a safe operation.
Exercises
Exercise 1: diagnosing an inflated repository
Diego clones task-manager and gets this:
$ git count-objects -vH count: 0 size: 0 bytes in-pack: 3847 packs: 1 size-pack: 512.84 MiB $ ls -lh assets/ -rw-r--r-- 1 diego diego 130 Aug 1 09:14 mockup-panel.psd -rw-r--r-- 1 diego diego 84M Aug 1 09:14 mockup-list.psd -rw-r--r-- 1 diego diego 118M Aug 1 09:14 demo.mp4
Answer:
A. Why does mockup-panel.psd weigh 130 bytes and the other two do not?
B. Why does the repository take up 512 MB if they supposedly use LFS?
C. Which commands would you run to confirm your diagnosis?
D. What is the complete solution?
Exercise 2: configuring LFS from scratch, in the right order
The team is going to add to task-manager:
assets/designs/*.psd— design files, ~80 MB, change weekly, iterated onassets/video/demo.mp4— 120 MB, redone wholesale every six monthsassets/icons/*.svg— text, a few KB eachassets/icons/generated/*.png— exported automatically from the SVGsdist/task-manager.zip— distribution package, generated for every version
Write the complete sequence of commands and files, deciding for each type whether it goes to LFS, to ordinary Git, to .gitignore or outside the repository. Justify each decision and respect the correct order of the commits.
Exercise 3: migration with obstacles
task-manager has had .psd and .mp4 files versioned directly for two years. The repository weighs 3.2 GB. The decision is taken to migrate to LFS.
Constraints:
- There are three open proposals from Diego.
mainis protected and requires review.- There are tags
v1.0.0tov1.7.0, andv1.6.0is what is in production. - Bruno has a local branch with five unpushed commits.
- The commits are signed (08-05).
Write the complete plan, in order, indicating what happens with each constraint and what checks you would make. Point out at exactly which stage the process becomes irreversible.
Solutions
Solution 1
A. mockup-panel.psd is an LFS pointer; the other two are real files versioned directly in Git.
Only the first was declared in .gitattributes before being added. The other two were either added before LFS was configured, or their pattern was never declared.
B. Because the 512 MB are the .psd and .mp4 files that are NOT in LFS, with all their historical versions. Adopting LFS halfway reduces nothing: the repository carries on dragging along everything that went in before or outside the patterns.
C. Diagnosis step by step:
# 1. What is actually in LFS?
git lfs ls-files
# Only mockup-panel.psd will appear
# 2. Which patterns are declared?
git lfs track
cat .gitattributes
# Probably only "assets/mockup-panel.psd", or "*.psd" added late
# 3. What is taking up the history? (lesson 08-06)
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '$1=="blob" {print $3, $4}' \
| sort -rn | head -15
# 4. The aggregated view by extension, which is the definitive one
git lfs migrate info --everything --above=1MbStep 4 will give something like:
That 9/12 is the key clue: of twelve .psd files in the history, nine are outside LFS.
D. The complete solution, in two parts:
Part 1: fix the configuration for the future.
git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes
git commit -m "chore: GT-262 declare all binaries in LFS"Part 2: clean up the history (a coordinated rewrite, section 9).
git lfs migrate import --everything --include="*.psd,*.mp4"
git lfs push --all origin
git push --force-with-lease --all origin
git push --force-with-lease --tags originAnd warn the team that they have to clone again. Without part 2 the repository will carry on weighing 512 MB forever, because the old objects do not disappear when you change the configuration.
Solution 2
Decisions, with their reasons:
| File | Decision | Why |
|---|---|---|
assets/designs/*.psd |
LFS | Large binary, changes often, the history is needed, it is not derived |
assets/video/demo.mp4 |
Outside the repository | Its history is not needed (it is redone wholesale). A reference in the README.md with the URL and hash |
assets/icons/*.svg |
Ordinary Git | It is text: it diffs, merges and compresses well. LFS would be an encumbrance |
assets/icons/generated/*.png |
.gitignore |
Derived from the SVGs. Generated in CI |
dist/task-manager.zip |
.gitignore |
A build artefact (lesson 08-03). Published as a release artefact |
On the .mp4: if the team prefers to version it, LFS is a defensible option. But since its history is not needed, keeping it outside avoids consuming quota with two 120 MB copies.
The complete sequence, in the right order:
cd ~/projects/task-manager
# --- Step 1: installation (once per machine, each person) ---
git lfs install
# --- Step 2: declare the LFS patterns ---
git lfs track "assets/designs/*.psd"
# --- Step 3: .gitignore for the derived files ---
cat >> .gitignore <<'EOF'
# Generated assets (GT-263)
assets/icons/generated/
dist/
EOF
# --- Step 4: check what has been declared BEFORE committing ---
git lfs track
cat .gitattributes
# --- Step 5: commit the CONFIGURATION first, on its own ---
git add .gitattributes .gitignore
git commit -m "chore: GT-263 configure Git LFS for the design files
The .psd files in assets/designs/ move to being managed with Git LFS:
only text pointers remain in the history and the content lives in the
server's LFS store.
The generated PNGs and the distribution package are ignored: they are
derived and the integration pipeline produces them.
The demonstration video is not versioned; its URL and its hash are
documented in the README.
Requirement: run 'git lfs install' before cloning."Committing the configuration on its own and first is what guarantees the filters are active by the time the binaries arrive.
# --- Step 6: now, at last, the files ---
git add assets/designs/panel.psd assets/designs/list.psd
git add assets/icons/*.svg
# --- Step 7: verify BEFORE committing ---
git lfs statusObjects to be committed:
assets/designs/panel.psd (LFS: 4d7a9f2)
assets/designs/list.psd (LFS: 9e2f7a4)This check is the key to the exercise. If it said (File: ...) instead of (LFS: ...), the filter would not have been applied and you would have to undo the add, review .gitattributes and start again. Verifying here costs five seconds; discovering it later costs a history rewrite.
git commit -m "feat: GT-263 add the panel and list designs"
# --- Step 8: confirm that the history holds pointers, not binaries ---
git show HEAD:assets/designs/panel.psd
git cat-file -s HEAD:assets/designs/panel.psd # should give ~130
# --- Step 9: publish ---
git push origin main
git lfs ls-filesAnd in CONTRIBUTING.md:
## Before cloning
This repository uses Git LFS for the design files.
1. Install git-lfs: `apt install git-lfs` / `brew install git-lfs`
2. Run once: `git lfs install`
3. Clone as normal.
If a `.psd` weighs 130 bytes and starts with `version https://git-lfs...`,
you have a pointer. Run `git lfs pull`.
The demonstration video is not in the repository: download it from the URL
given in the README and verify its hash.Solution 3
The complete plan, by phases.
Phase 0 — Preparation (reversible).
With that figure you decide whether it is worth it. Going from 3.2 GB to ~150 MB, yes; to 2.8 GB, no.
Constraint — Diego's three proposals: they have to be closed first. They get integrated or abandoned. A proposal based on commits that are about to stop existing is useless, and asking an external contributor to redo three branches on top of a rewritten history is a bad experience. This is the constraint that sets the schedule.
Constraint — Bruno's local branch: he should push it to the server before the migration. That way it will be rewritten along with everything else and he will not have to replant it by hand. If he cannot, he will have to do the rebase --onto from section 9.
# Each person checks that they have nothing left
git log --branches --not --remotes --oneline
git stash list
git statusPhase 1 — Backups (reversible).
git clone --mirror https://git.example.com/team/task-manager.git ~/backups/before.git
git bundle create ~/backups/before-$(date +%F).bundle --all
# Verify that the backup is genuinely usable
git bundle verify ~/backups/before-$(date +%F).bundleAnd, in case the original binaries are ever needed, download the content before losing sight of it.
Phase 2 — Rehearsal (reversible).
git clone https://git.example.com/team/task-manager.git /tmp/rehearsal
cd /tmp/rehearsal
git lfs migrate import --everything --include="*.psd,*.mp4"
# How much has it shrunk?
git count-objects -vH
# Cryptographic verification: the CODE has not changed
git rev-parse HEAD:src
git -C ~/projects/task-manager rev-parse HEAD:src # should match
# Does v1.6.0 still have the same code content?
git rev-parse v1.6.0^{tree}
git cat-file -p v1.6.0^{tree}
# Note down the mapping between old and new tags
git for-each-ref --format='%(refname:short) %(objectname:short)' refs/tagsThat mapping table has to be kept and published: if somebody has noted down that commit 3f2a1b9 is in production, they will need to know what its new equivalent is.
Constraint — the signatures: on rewriting the commits, the signatures become invalid and are lost. The commit object changes, so the signature over the previous object stops being valid. It has to be accepted and documented: the commits from before the migration will be left with no verifiable signature. It is a serious argument for doing the migration as soon as possible or never doing it at all, and one more reason to configure LFS on day one. Subsequent commits will be signed as normal.
Constraint — v1.6.0 in production: the tag will change hash. You have to check what points at that commit in the deployment system (lesson 10-05) and update it. And verify that the new v1.6.0 produces the same artefact as the old one: same code, different commit hash.
Phase 3 — Migration window (THE IRREVERSIBLE POINT).
The point of no return is the
git push --force-with-lease. Everything before it is reversible. From there on, the server has the new history and anybody who clones or fetches gets the rewritten version. Going back requires restoring from the mirror backup and repeating the announcement.
# 1. Announce the start and ask that nobody pushes anything
# 2. Temporarily unprotect main on the server <-- protected branch constraint
# 3. Migrate
cd ~/projects/task-manager
git fetch --all --prune
git lfs migrate import --everything --include="*.psd,*.mp4"
# 4. FIRST the LFS content
git lfs push --all origin
# 5. Verify that the store has it before touching the references
git lfs ls-files --all | wc -l
# 6. NOW the references <-- IRREVERSIBLE from here on
git push --force-with-lease --all origin
git push --force-with-lease --tags origin
# 7. Protect main againThe protected branch requires unprotecting and reprotecting. Write it into the script: it is the step that gets forgotten most, and leaving main unprotected over a weekend is an unnecessary risk.
Phase 4 — The team recovers.
The message to the team:
The history of task-manager has been rewritten to migrate the binaries to LFS. The repository has gone from 3.2 GB to ~150 MB. WHAT YOU HAVE TO DO: 1. git lfs install (if you have not already) 2. Rename your current clone and clone again. 3. git lfs pull DO NOT run 'git pull' on your old clone: it will produce a total divergence. The tags have changed hash. Mapping: <link> Commits from before today have lost their signature; new ones are signed as before. If you have unpushed local work, tell me before touching anything.
Phase 5 — Closing.
And on the server: request garbage collection so that it frees the space of the old objects. Without this step, the server's repository will carry on weighing 3.2 GB and the migration will look as though it achieved nothing.
Finally, put the guard in CI (tip 2) so that this does not happen again, and document LFS in CONTRIBUTING.md.
Conclusion
Git is excellent for text and terrible for large binaries, and now you know exactly why: it is not an arbitrary limitation, it is a consequence of the data model. Every version is a complete blob, deltas do not work on already-compressed content, objects are immutable and everybody downloads the entire history.
- Git LFS replaces the file with a text pointer of about 130 bytes —
version,oid sha256:...,size— which does version well, and stores the real content in a separate store. It works by means of thecleanandsmudgefilters of.gitattributes, the same mechanism as lesson 08-04. - The configuration lives in
.gitattributesasfilter=lfs diff=lfs merge=lfs -text: thefilterdoes the substitution,diffandmergeregister LFS's drivers, and-textstops line-ending normalisation corrupting the binary. Being versioned, the whole team inherits it; what is not inherited is the LFS installation. - Order matters:
git lfs track, commit.gitattributeson its own, and then add the binaries. Always verify withgit lfs statusthat it says(LFS: ...)and not(File: ...). - On cloning, the pointers get downloaded and only the content of the checked-out revision.
GIT_LFS_SKIP_SMUDGE=1andlfs: falsein CI avoid unnecessary downloads;git lfs pullbrings them down when they are needed. Andgit lfs fetch --alldownloads the entire history of content: use it only for backups. git lfs migrate importrewrites the history, with the full force of the golden rule of lesson 05-01: the hashes change, the tags change, the signatures become invalid, and everybody has to clone again. Rehearse on a copy, verify with the tree hash that the code has not been touched, publish the LFS content first and the references afterwards, and do not forget to request the server's garbage collection.- The limitations are real and have to be known beforehand: a server is needed, quotas cost money (download bandwidth too), platforms do not behave the same, the clone stops being self-sufficient, and LFS files do not merge: you have to choose a winning version, or coordinate with
--lockableandgit lfs lock. - And there are alternatives:
git annexfor more flexible scenarios, keeping the binary outside with a reference when you do not need its history, and above all rethinking the workflow: if the file is generated, it goes into.gitignore; if you can version the source instead of the result, do it.
The idea that sums up the lesson:
LFS does not make Git handle binaries well. It makes Git stop handling them, versioning a reference in their place. It is an excellent solution when the binary is genuinely part of the product and you need its history, and unnecessary complexity in any other case.
For task-manager, with its four text files, LFS was irrelevant until yesterday. With the arrival of the design files it has become necessary, and it has arrived just in time: configuring it today costs two commits; doing it a year from now would have cost a history rewrite and an afternoon of coordination.
What is coming
LFS solves one of the three ways in which a repository becomes unmanageable: very large files. The other two remain, and they are independent.
A repository can have a colossal amount of history — twenty years and hundreds of thousands of commits — even if every file is tiny: cloning it takes an eternity and git log crawls. And it can have a colossal number of files — hundreds of thousands in the working copy — even if the history is short: git status takes half a minute because it has to walk the whole tree.
Each one has its own remedy, and none of them helps with the other two. Partial clones with --filter=blob:none attack the volume of content downloaded. sparse-checkout with a sparse index attacks the number of files on disk. The commit-graph attacks the cost of walking the history. And above all of it is the decision that conditions the rest: monorepo or multi-repo, the comparison lesson 06-05 left open and that case 3 of 10-01 left waiting to be explained.
Lesson 10-04: Scaling Git for Large Projects delivers on both promises, and it starts with the only rule that matters in optimisation: measure before you touch anything.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
