Your Code. Your Rules.

Should Have Been Pointers

Encountered N files that should have been pointers, but weren't - files committed in full instead of as Git LFS pointers, and how to convert them.

Files That Should Have Been Pointers

Symptom

The clone works, but Git LFS prints a warning:

Cloning into '<REPO>'...
Encountered 2 files that should have been pointers, but weren't:
	test.psd
	test2.psd

Right after the clone, git status already lists the files as changed, and they stay that way:

Changes not staged for commit:
	modified:   test.psd
	modified:   test2.psd

The push that brought the files in printed no error: git.eu does not check LFS pointers when it receives a push.

Cause

A .gitattributes rule sends the path to Git LFS (filter=lfs), but the commit holds the full file instead of the small LFS pointer. This happens when:

  • the file was committed before the .gitattributes rule existed;
  • the commit was made on a machine without git-lfs, or where git lfs install never ran;
  • the history was imported from another forge or repository that did not store the file in Git LFS.

Git LFS expects every blob under such a rule to be a pointer like this one:

version https://git-lfs.github.com/spec/v1
oid sha256:<64 hex characters>
size <bytes>

The blob is the full file instead, so Git LFS warns at checkout. Its clean filter turns the file on disk into a pointer that differs from the committed blob, so git status reports it as modified.

The files on disk are complete and nothing is lost. The server holds no LFS object for them, and every new version makes the repository bigger.

Diagnose

1. Check that a rule sends the file to Git LFS:

git check-attr -a -- test.psd

filter: lfs in the output means the rule covers the file.

2. List the files Git LFS tracks:

git lfs ls-files

The affected files are not listed at all. A listed file shows * (object present locally) or - (pointer only, object not downloaded): both are healthy.

3. Find every file that should be a pointer:

git lfs fsck --pointers

It prints one line per affected file and exits with status 1:

pointer: unexpectedGitObject: "test.psd" (treeish 22a1c46…) should have been a pointer but was not
pointer: unexpectedGitObject: "test2.psd" (treeish 22a1c46…) should have been a pointer but was not

Fix

Pick one option and fix every branch that holds the files. Options A and B add a commit; older commits keep the full files.

Option A - Renormalize (recommended)

This step runs the LFS clean filter on the files again, so the index holds pointers. It adds a new commit and does not rewrite history. test.psd and test2.psd stand for your files.

git add --renormalize test.psd test2.psd
git status
git commit -m "Convert test.psd and test2.psd to Git LFS pointers"
git push

git status now lists the files as staged (modified:). To cover every file that a .gitattributes rule matches, run git add --renormalize . instead. The push uploads the LFS objects:

Uploading LFS objects: 100% (2/2), 358 KB | 0 B/s, done.

Option B - Migrate without rewriting history

This command turns the named paths into pointers and creates the commit itself, so do not run git commit afterwards. History is not rewritten.

git lfs migrate import --no-rewrite test2.psd
git push

It prints migrate: checkout: ..., done. and adds the commit test2.psd: convert to Git LFS.

  • The .gitattributes rule must exist first. Without it the command stops with No Git LFS filters found in '.gitattributes' (exit status 2).
  • Give at least one path. Without one it stops with Expected one or more files with --no-rewrite.
  • Every run adds one more commit, even an identical one: run it once, with every path in the same command.

Option C - Purge the files from history (⚠ rewrites history)

Use it only when the full files must leave the history, for example to shrink the repository.

This converts the files in every commit of every local branch, then force-pushes the result:

git lfs migrate import --everything --include="*.psd" --yes
git push --force-with-lease origin main
migrate: Rewriting commits: 100% (2/2), done.
migrate: Updating refs: ..., done.
Uploading LFS objects: 100% (1/1), 51 KB | 0 B/s, done.
 + 9b5abe9...1f4d751 main -> main (forced update)

Push every other rewritten branch and tag the same way (git push --force-with-lease --all origin, then git push --force-with-lease --tags origin).

Verify

git lfs fsck --pointers
git lfs ls-files

git lfs fsck --pointers prints Git LFS fsck OK, and git lfs ls-files lists each file, for example 72c7c7a20a - test.psd. A fresh clone prints no warning.

After Option A or B, older commits still hold the full files: git lfs fsck --pointers HEAD~3..HEAD still reports them in that range. Only Option C removes them.

Prevent

  • Install git-lfs and run git lfs install on every machine that commits to the repository.
  • Commit the .gitattributes rule (git lfs track "*.psd") before the first large file.
  • Run git lfs fsck --pointers in CI, so a full file is caught before it spreads.

Related