Multi-Action Commits API does not respect Git LFS rules
Summary
POST /projects/:id/repository/commits (the multi-action Commits API) does not create a Git LFS pointer for a file matching .gitattributes, specifically when the same request also creates the target branch via start_branch. The single-file POST /projects/:id/repository/files/:file_path endpoint does not have this problem — only the multi-action Commits API does.
This breaks any Git-based CMS/tool that atomically creates a feature branch and commits an LFS-tracked file in one call — a very common pattern for editorial/PR-style content workflows (e.g. Sveltia CMS's GitLab backend does exactly this on every new entry).
Environment
- GitLab CE, self-managed, version 19.4.0
- Also present on
masteras of 2026-09-21 (confirmed by reading current source — see Root cause)
Steps to reproduce
Given a repository with .gitattributes on branch main containing:
assets/** filter=lfs diff=lfs merge=lfs -textPOST /projects/:id/repository/commitswith a new branch, creating it in the same call:{ "branch": "feature-x", "start_branch": "main", "commit_message": "add asset", "actions": [ { "action": "create", "file_path": "assets/image.bin", "content": "<base64 binary>", "encoding": "base64" } ] }- Fetch the committed file:
GET /projects/:id/repository/files/assets%2Fimage.bin?ref=feature-x
Actual result
content is the raw file (size equals the original byte length). Not an LFS pointer.
Expected result
The file should be committed as a Git LFS pointer (130-byte text blob starting with version https://git-lfs.github.com/spec/v1), exactly as it would be had the file been added in a second commit to the already-existing feature-x branch, or via the single-file Files API with start_branch set.
Control case (works correctly)
Repeating step 1 with actions targeting an already-existing feature-x (no start_branch in the payload, since the branch now exists) correctly produces an LFS pointer. This isolates the trigger to "commit and branch-creation happening in the same request," not a general LFS/attributes misconfiguration.
Root cause
app/services/files/multi_service.rb:
def create_commit!
transformer = Lfs::FileTransformer.new(project, repository, @branch_name)
...This omits start_branch_name:. Compare with app/services/files/create_service.rb (used by the single-file Repository Files API), which correctly passes it:
def create_commit!
transformer = Lfs::FileTransformer.new(project, repository, @branch_name, start_branch_name: @start_branch)
...In app/services/lfs/file_transformer.rb:
def branch_to_base_off
@branch_to_base_off ||= start_branch_name || branch_name
end
def lfs_file?(file_path)
cached_attributes.attributes(file_path)['filter'] == 'lfs'
end
def cached_attributes
@cached_attributes ||= repository.attributes_at(branch_to_base_off)
endSince Files::MultiService never supplies start_branch_name, branch_to_base_off falls back to branch_name — the branch being created in this very request, which doesn't exist yet at the point attributes_at is called. .gitattributes lookup finds nothing, lfs_file? returns false, and the file is committed verbatim instead of as an LFS pointer.
The line has been unchanged since 2019 (commit f7163afb8a036468ccc3f657ac09f3c09b318dab).
Suggested fix
In Files::MultiService#create_commit!, pass start_branch_name: @start_branch to Lfs::FileTransformer.new, matching Files::CreateService.
Related
This was fixed for the single-file case in !139351 (merged)