Skip to content

LFS download in submodules fails: hardcoded .git/lfs/tmp path doesn't resolve gitlink file #61

Description

@thevinchi

Description

When a repo backed by git-remote-s3 is added as a Git submodule, git lfs pull (and the implicit checkout that follows git submodule add) fails with:

error transferring "<oid>": [2] [Errno 20] Not a directory: '<worktree>/<sub>/.git/lfs/tmp/<oid>.<suffix>'
Failed to fetch some objects from 'https://s3///<profile>@<bucket>/<prefix>.git/info/lfs'

In a submodule, <sub>/.git is not a directory — it is a gitlink file containing gitdir: ../../.git/modules/<path> pointing to the real git directory in the parent repo. Two locations in git_remote_s3/lfs.py build paths through .git/... as a literal string, which is invalid in this context.

Reproduction

  1. Create two S3-backed repos (call them parent and child) using the s3:// URL scheme.
  2. In child, track a file with Git LFS, commit, and push. Confirm git lfs ls-files lists it.
  3. In parent:
    git submodule add s3://<profile>@<bucket>/<child-prefix> sub
  4. Observe the bundle clone succeed (Unbundling objects: 100%), then the LFS materialization fail with [Errno 20] Not a directory. The submodule is left half-checked-out.

Manually running git lfs pull inside the submodule reproduces the same error after configuring the standalone transfer agent in the submodule's git config.

Root cause

In git_remote_s3/lfs.py:

  • L19 — module-level logging.basicConfig(filename=".git/lfs/tmp/git-lfs-s3.log") resolves .git/ against CWD. In a submodule this points at a regular file, not a directory.
  • L115LFSProcess.download() does temp_dir = os.path.abspath(".git/lfs/tmp") and writes {temp_dir}/{oid} (L118). Same problem.

Non-submodule clones work because .git happens to be a directory there. The git-lfs custom-transfer protocol does not include a path field in download events (per docs/custom-transfers.md), so the agent must construct the path itself — but it should use the resolved gitdir, not the literal string .git/.

Suggested fix

Resolve the gitdir via git rev-parse --absolute-git-dir at runtime:

def _git_dir() -> str:
    return subprocess.check_output(
        ["git", "rev-parse", "--absolute-git-dir"], text=True
    ).strip()

Then in download():

temp_dir = os.path.join(_git_dir(), "lfs", "tmp")
os.makedirs(temp_dir, exist_ok=True)

For the L19 log path, defer FileHandler setup into main() so the resolution happens after CWD/init is settled — or use the same _git_dir() helper. Optionally, honoring lfs.storage (per git-lfs config docs) would let users override the location explicitly.

Environment

  • git-remote-s3: 0.3.2 (PyPI)
  • git: 2.39.5
  • git-lfs: 3.7.1 (GitHub; linux arm64; go 1.24.4)
  • Python 3.11
  • Linux 6.1 (aarch64)

Workaround

Patch the two hardcoded paths in the installed git_remote_s3/lfs.py to use git rev-parse --absolute-git-dir instead of the literal .git/.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions