Skip to content

Find git-lfs in the user's PATH instead of hardcoding /usr/local/bin - #2835

Open
mluisbrown wants to merge 4 commits into
git-up:masterfrom
mluisbrown:fix-git-lfs-path
Open

mluisbrown wants to merge 4 commits into
git-up:masterfrom
mluisbrown:fix-git-lfs-path

Conversation

@mluisbrown

@mluisbrown mluisbrown commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #805.

GitUp only registered its Git LFS filter if git-lfs existed at exactly /usr/local/bin/git-lfs. With Homebrew on Apple Silicon (/opt/homebrew/bin) or MacPorts (/opt/local/bin), the filter was never registered. LFS files were then checked out as pointer files and committed as raw content instead of LFS pointers, with no error.

Changes

  • The lfs filter is now always registered. git-lfs is looked up the first time a file with the filter=lfs attribute is processed, and the result is cached. The lookup searches the login shell PATH (the same approach hooks already use), then falls back to /opt/homebrew/bin, /usr/local/bin and /opt/local/bin. Repositories that don't use LFS never run the lookup.
  • A new check callback returns GIT_PASSTHROUGH if git-lfs can't be found, so LFS files are left as pointer files. This preserves the previous behavior when LFS isn't installed.
  • git-lfs now runs with the login shell PATH, so the git it invokes is the same one the user gets on the command line.
  • The shell PATH lookup moved into a shared static function so the filter can call it without a repository instance. -getPATHUsingShell:error: now wraps it and behaves the same as before.

Speeding up checkouts with many LFS files

Once the filter was active, checkouts that touch many LFS files became very slow. Each file started its own git-lfs smudge or git-lfs clean, and each start-up runs several git commands, about 200 ms in total. Rebasing a branch onto a commit that changed 346 LFS-tracked snapshot images took about 1.5 minutes. git checkout takes 2.2 s for the same change.

  • Like the git command line, GitUp now runs a single long-running git-lfs filter-process for each working directory, using Git's long-running filter protocol version 2. The process is reused for later files and closed after 10 s idle. That checkout now takes 1.25 s.
  • If a cached idle process turns out to have exited, it's replaced with a new one. If filter-process can't be used at all (e.g. git-lfs older than 2.0), GitUp runs git-lfs for each file as before.
  • Errors now include what git-lfs wrote to stderr, so a failed download says why, e.g. git-lfs filter process failed: … Smudge error: Error downloading … batch request: missing protocol. When git-lfs can't download a file it replies status=success and then exits partway through. That's reported as an error for the file, and the download isn't retried.

Testing

  • Added testGitLFSCleanFilter. It checks that adding an LFS-tracked file to the index stores an LFS pointer, and it skips itself if git-lfs isn't installed. I confirmed that it fails against the old code when nothing exists at /usr/local/bin/git-lfs, and that it passes with this change.
  • Added testFindExecutableInPATH, which covers the PATH lookup with fake executables in temporary directories, independent of whether git-lfs is installed.
  • Added testGitLFSFilterRoundTrip. It sends 20 files, some spanning several protocol packets, through clean and then smudge. It asserts that all of them go through one filter process with no per-file fallbacks.
  • Added testGitLFSFilterError. It checks that smudging a pointer to a missing object reports the git-lfs error without falling back, and that the next file gets a new process.
  • The filter-process tests use the real git-lfs and skip themselves if it isn't installed.
  • GitUpKit (macOS) tests: everything passes except two tests that also fail without this change. testDateFormatting ("This Monday" vs "Last Monday") depends on the current date. testClone fails at its SSH clone from GitHub in my environment.

The wiki currently says git-lfs must be installed at /usr/local/bin/git-lfs, so it should be updated after this is merged.

🤖 Generated with Claude Code

The LFS filter was only registered if git-lfs existed at /usr/local/bin/git-lfs,
so with Homebrew on Apple Silicon (/opt/homebrew/bin) or MacPorts (/opt/local/bin)
LFS files were silently left unfiltered (git-up#805).

The filter is now always registered and git-lfs is resolved lazily, the first time
a file with the lfs filter attribute is processed, from the login shell PATH with
fallbacks to the common install locations. If git-lfs isn't found the filter
passes through, preserving the previous behavior. git-lfs is also run with the
login shell PATH so it uses the same git as the command line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; both previous findings are addressed and no new actionable issue was found.

Summary

This PR finds git-lfs through the user's shell PATH, with common install directories as fallbacks, and reuses a filter process for each working directory.

The latest changes:

  • GitUp finds Git LFS in the user's shell path for LFS files.
  • GitUp reuses a Git LFS filter process for repeated file operations.

Both numbered findings from the previous review are addressed.

Reviews (4) · Last reviewed commit: "Report git-lfs errors and check that the..." · Reviewed by Greptile

Comment thread GitUpKit/Core/GCRepository-Tests.m Resolved
mluisbrown and others added 2 commits October 16, 2026 14:33
Extract the lookup into GCFindExecutableInPATH() so it can be tested with a
controlled PATH and fake executables, independent of whether git-lfs is
installed or present at /usr/local/bin. Also ignore directories named like the
tool, which -isExecutableFileAtPath: would otherwise accept.

The real git-lfs test remains as a smoke test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Executing "git-lfs clean" or "git-lfs smudge" for each file costs ~200ms as
git-lfs runs several git commands on startup, so checking out hundreds of LFS
files (e.g. when rebasing onto a commit that changed many snapshot images) took
minutes instead of the couple of seconds the git command line needs.

Like the git command line, talk to a single "git-lfs filter-process" per working
directory using the long-running filter protocol version 2 and terminate it
once idle. Fall back to executing git-lfs per file if the process can't be used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread GitUpKit/Core/GCRepository.m Resolved
Comment thread GitUpKit/Core/GCRepository-Tests.m Resolved
Address review feedback on the long-running filter process:

- Capture what git-lfs writes to stderr, draining it continuously so the pipe
  can't fill up, and include it in the error so e.g. a failed download says
  why instead of only "git-lfs filter process failed". The per-file fallback
  now does the same.
- git-lfs replies "status=success" then exits when it can't download a file.
  Report that as an error for the file instead of retrying it by executing
  git-lfs again, which would repeat the failing download.
- Retry with a new process if a cached idle one turns out to have exited,
  before falling back to executing git-lfs per file.
- Count filter process launches and per-file fallbacks so the tests can check
  that many files go through a single process, and that a failure reports the
  git-lfs error without falling back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

git-lfs post commit hook error, but only in app

1 participant