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 master as 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 -text
  1. POST /projects/:id/repository/commits with 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" }
      ]
    }
  2. 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)
end

Since 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.

This was fixed for the single-file case in !139351 (merged)

Edited by 🤖 GitLab Bot 🤖