GitLab.comで公開されているリポジトリを会社のGitLabサーバーにpushしたところ、 「LFS objects are missing」エラーが発生してハマったのでその原因と回避策について説明します。
現象
問題が発生したのは こちらのリポジトリ になります。 このリポジトリを会社の GitLab サーバーに保存するため、以下のような操作を行いました。 なお、 gitlab.example.com
は会社の GitLab サーバー、 myuser
は自分のユーザー名を表しています。
$ git clone git@gitlab.com:ApexAI/performance_test.git
$ cd performance_test
$ git remote set-url origin git@gitlab.example.com:myuser/performance_test.git
$ git push --all origin
すると次のようなエラーメッセージが表示されて push が失敗しました。
remote: Resolving deltas: 100% (1106/1106), done.
remote: GitLab: LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".
To gitlab.example.com:myuser/peformance_test.git
! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'git@gitlab.example.com:myuser/peformance_test.git'
回避策
この問題の回避策は2通りあります。
1つ目は、GitLab のフィーチャーフラグを使用する方法です。 この方法を利用できるのは、 GitLab サーバーの管理者のみとなります。
具体的には GitLab サーバーに SSH でログインして、下記のコマンドを実行します。
$ sudo gitlab-rails console
> Feature.disable(:lfs_check)
2つ目は、プロジェクトの設定で LFS を無効化する方法です。 この方法を利用できるのは、 対象プロジェクトの Maintainer 以上の権限を持つユーザーとなります。
具体的には、プロジェクトメニューの Settings > General > Visibility, project features, permissions にある Git Large File Storage をオフにすることで変更できます。
続いて原因の調査方法について説明しますが、かなり長くなるのでご注意ください。 興味の無い方は、以降の内容は読み飛ばしください。
原因の調査
エラーメッセージから LFS オブジェクトが適切に push されていないのが原因のようですが、 LFS を使用している場合に存在するはずの .gitattributes
ファイルがリポジトリに存在しません。 不思議に思いながら、表示されているように git lfs push --all
を試してみます。
$ git lfs push --all origin
すると、以下の様なエラーメッセージが表示されて LFS オブジェクトの push が失敗しました。
(missing) performance_test/bin/fastrtpsgen.jar (189fa3ed5db458d4de938e94b1488e1bb60bdcdd76d1185e722c23d3d1860e92)
hint: Your push was rejected due to missing or corrupt local objects.
hint: You can disable this check with: 'git config lfs.allowincompletepush true'
Uploading LFS objects: 0% (0/1), 0 B | 0 B/s, done.
performance_test/bin/fastrtpsgen.jar
ファイルが見つからないためにエラーになっているようですが、 該当のファイルはワーキングディレクトリの中には存在しています。
やはり、何が起きているのかよく分かりません。
とりあえず、ヒントとして表示されている git config lfs.allowincompletepush true
を実行してから、もう一度 LFS オブジェクトを push してみます。
$ git config lfs.allowincompletepush true
$ git lfs push --all origin
コマンドでエラーは発生しなくなりましたが、次のようなメッセージは表示されたままです。
LFS upload missing objects: (0/1), 0 B | 0 B/s
(missing) performance_test/bin/fastrtpsgen.jar (189fa3ed5db458d4de938e94b1488e1bb60bdcdd76d1185e722c23d3d1860e92)
Uploading LFS objects: 0% (0/1), 0 B | 0 B/s, done.
とりあえず、もう一度 push してみます。
$ git push --all origin
やはり、はじめと同じエラーメッセージが表示されて push が失敗しました。
ここで手詰まりとなったので push の時に表示されるエラーメッセージを手がかりに GitLab のソースを調べることにしました。 GitLab プロジェクト を開いて、 画面右上の検索ボックスに「LFS objects are missing」に入力してエラーを出力している場所を探します。
すると lfs_check.rb で、次のようにエラーメッセージが定義されているのが見つかります。
class LfsCheck < BaseChecker
LOG_MESSAGE = 'Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...'
ERROR_MESSAGE = 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
def validate!
return unless Feature.enabled?(:lfs_check, default_enabled: true)
return unless project.lfs_enabled?
return if skip_lfs_integrity_check
logger.log_timed(LOG_MESSAGE) do
lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left)
if lfs_check.objects_missing?
raise GitAccess::ForbiddenError, ERROR_MESSAGE
end
end
end
end
このコードから以下のいずれかの場合に LFS オブジェクトのチェックをスキップできそうなことが分かりました。
Feature.enabled?(:lfs_check, default_enabled: true)
がfalse
の場合project.lfs_enabled?
がfalse
の場合skip_lfs_integrity_check
がtrue
の場合
Feature.enabled?(:lfs_check, default_enabled: true)
は GitLab のフィーチャフラグの判定ロジックで、 サーバー管理者がコンソールでオン/オフの切替ができます。 GitLab のフィーチャーフラグについては こちらのドキュメント に詳しく書かれているので、興味のある方はそちらを参照ください。
それでは :lfs_check
フィーチャーフラグの設定をオフに変更します。 そのためには GitLab サーバーに SSH でログインして次のコマンドを実行します。
$ sudo gitlab-rails console
> Feature.disable(:lfs_check)
これで LFS オブジェクトのエラーをスキップできるようになるので、もう一度 push してみます。
$ git push --all origin
今度はエラーが発生せずに無事に push することができました。
フィーチャーフラグの設定を変更して LFS オブジェクトのエラーを回避することができましたが、 フィーチャーフラグは実験的な機能のオン/オフを切り替えるのが目的のため、 将来的に削除される可能性があります。 また、フィーチャーフラグの設定を変更できるのはサーバー管理者だけで、一般ユーザーには変更できません。
その場合は project.lfs_enabled?
が false
を返すように設定することで、エラーを回避することができます。 この設定はプロジェクトメニューの Settings > General > Visibility, project features, permissions にある Git Large File Storage をオフにすることで変更できます。
前述の2つの方法で LFS のエラーを回避することが分かりましたが、そもそもなぜ LFS のエラーが発生するかの原因は不明なままです。 せっかくなので、エラーが発生する原因を深堀りしてみます。
前述のコードから lfs_check.objects_missing?
が true
を返すと LFS のエラーが発生することが分かります。 objects_missing?
の 定義 は次のようになっています。
class LfsIntegrity
def initialize(project, newrev, time_left)
@project = project
@newrev = newrev
@time_left = time_left
end
def objects_missing?
return false unless @newrev && @project.lfs_enabled?
new_lfs_pointers = Gitlab::Git::LfsChanges.new(@project.repository, @newrev)
.new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT, dynamic_timeout: @time_left)
return false unless new_lfs_pointers.present?
existing_count = @project.all_lfs_objects
.for_oids(new_lfs_pointers.map(&:lfs_oid))
.count
existing_count != new_lfs_pointers.count
end
end
上記のコードから existing_count != new_lfs_pointers.count
の場合に LFS のエラーが発生することが分かります。 さらに LfsChanges
クラスの 定義 を深堀りしてみます。
class LfsChanges
def initialize(repository, newrev = nil)
@repository = repository
@newrev = newrev
end
def new_pointers(object_limit: nil, not_in: nil, dynamic_timeout: nil)
@repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in, dynamic_timeout)
end
def all_pointers
@repository.gitaly_blob_client.get_all_lfs_pointers
end
end
@repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in, dynamic_timeout)
が何を返すのかさらに 定義 を深堀りしてみます。
def get_new_lfs_pointers(revision, limit, not_in, dynamic_timeout = nil)
request = Gitaly::GetNewLFSPointersRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision),
limit: limit || 0
)
if not_in.nil? || not_in == :all
request.not_in_all = true
else
request.not_in_refs += not_in
end
timeout =
if dynamic_timeout
[dynamic_timeout, GitalyClient.medium_timeout].min
else
GitalyClient.medium_timeout
end
response = GitalyClient.call(
@gitaly_repo.storage_name,
:blob_service,
:get_new_lfs_pointers,
request,
timeout: timeout
)
map_lfs_pointers(response)
end
ここで GitalyClient.call
というメソッドが出てきます。 詳しい説明は省略しますが、これは GitLab で Git 関連の操作を受け持つ Gitaly と呼ばれるサービスに get_new_lfs_pointers
という gRPC のリクエストを送信していることを表しています。 Gitaly の詳細は こちらのドキュメント を参照ください。
続いて、 Gitalyプロジェクト で get_new_lfs_pointers
の 定義 を深堀りしてみます。
def get_new_lfs_pointers(request, call)
Enumerator.new do |y|
changes = lfs_changes(request, call)
object_limit = request.limit.zero? ? nil : request.limit
not_in = request.not_in_all ? :all : request.not_in_refs.to_a
blobs = changes.new_pointers(object_limit: object_limit, not_in: not_in)
sliced_gitaly_lfs_pointers(blobs) do |lfs_pointers|
y.yield Gitaly::GetNewLFSPointersResponse.new(lfs_pointers: lfs_pointers)
end
end
end
# 省略
private
def lfs_changes(request, call)
repo = Gitlab::Git::Repository.from_gitaly(request.repository, call)
Gitlab::Git::LfsChanges.new(repo, request.revision)
end
LfsChanges
クラスで LFS ポインターの変更を取得していそうなことが分かります。 LfsChanges
の 定義 をさらに深堀りします。
def git_new_pointers(object_limit, not_in)
rev_list.new_objects(rev_list_params(not_in: not_in)) do |object_ids|
object_ids = object_ids.take(object_limit) if object_limit
Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
end
end
上記のような git_new_pointers
で LFS ポインターを取得していそうなことが分かります。 Blob.batch_lfs_pointers
の 定義 をさらに深堀りします。
# Find LFS blobs given an array of sha ids
# Returns array of Gitlab::Git::Blob
# Does not guarantee blob data will be set
def batch_lfs_pointers(repository, blob_ids)
blob_ids.lazy
.select { |sha| possible_lfs_blob?(repository, sha) }
.map { |sha| rugged_raw(repository, sha, limit: LFS_POINTER_MAX_SIZE) }
.select(&:lfs_pointer?)
.force
end
# 省略
# Valid LFS object pointer is a text file consisting of
# version
# oid
# size
# see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer
def lfs_pointer?
self.class.size_could_be_lfs?(size) && has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
end
def lfs_oid
if has_lfs_version_key?
oid = data.match(/(?<=sha256:)([0-9a-f]{64})/)
return oid[1] if oid
end
nil
end
def lfs_size
if has_lfs_version_key?
size = data.match(/(?<=size )([0-9]+)/)
return size[1].to_i if size
end
nil
end
# 省略
private
def has_lfs_version_key?
!empty? && text? && data.start_with?("version https://git-lfs.github.com/spec")
end
詳細は省きますが、上記のコードから次のような Blob オブジェクトを LFS ポインターとして判定していることが分かります。 Blob オブジェクトなどの Git オブジェクトの仕様は こちらのドキュメント を参照ください。 また、 LFS ポインターなどの Git LFS の詳細な説明は こちらの記事 を参照ください。
version https://git-lfs.github.com/spec/v1
oid sha256:44a8486abdf0330a3fe6b586e407506e738fe075fe0b5dfc43961e358fda7206
size 2684586
以上から、 push した時に LFS オブジェクトが見つからないエラーとなった原因は次のように考えられます。
- push された Blob オブジェクトに LFS ポインターが存在する
- LFS ポインターが指し示すファイルの実体が存在しない
それでは、本当にこの推測が正しいのか確認してみます。 まずは、次のシェルスクリプトで該当リポジトリに存在する LFS ポインターを検索します。 ファイル名は get-all-lfs-pointers.sh
として、実行権限を付与してパスの通ったディレクトリに保存してください。
#!/bin/sh
git rev-list --all --filter=blob:limit=200 --in-commit-order --objects \
| while read object path ; do
type=`git cat-file -t $object`
if [ "$type" = "blob" ]; then
if git cat-file -p $object | grep -q -e "^version https://git-lfs.github.com/spec" ; then
echo "*********** LFS pointer is found *********"
echo "Blob: $object, Path: $path"
echo "*********** Start content *********"
git cat-file -p $object
echo "*********** End content *********"
fi
fi
done
続いて、 performance_test
のワーキングディレクトリに移動して次のコマンドを実行します。
$ get-all-lfs-pointers.sh
すると次のように表示され、 LFS ポインターが見つかりました。
*********** LFS pointer is found *********
Blob: 987f780ba6c01f746b83b4f5718fe0e6f70a0e7d, Path: performance_test/bin/fastrtpsgen.jar
*********** Start content *********
version https://git-lfs.github.com/spec/v1
oid sha256:189fa3ed5db458d4de938e94b1488e1bb60bdcdd76d1185e722c23d3d1860e92
size 2353057
*********** End content *********
続いて、 Blob: 987f780ba6c01f746b83b4f5718fe0e6f70a0e7d
が含まれるコミットを探します。 そのため、次のようなシェルスクリプトを作成しました。 作成したファイルは find-commit-from-blob.sh
という名前で保存し、実行権限を付与してパスの通ったディレクトリに配置してください。
#!/bin/sh
obj_name="$1"
shift
git log "$@" --pretty=format:'%T %h %s' \
| while read tree commit subject ; do
if git ls-tree -r $tree | grep -q "$obj_name" ; then
echo $commit "$subject"
fi
done
続いて、次のコマンドを実行します。
$ find-commit-from-blob.sh 987f780ba6c01f746b83b4f5718fe0e6f70a0e7d
すると次のように表示され、該当の Blob オブジェクトを含むコミットが見つかりました。
008c746 Adding the code.
それでは、該当のコミットをチェックアウトして確認してみましょう。
$ git checkout 008c746
ワーキングディレクトリの中身を確認すると、 performance_test/bin/fastrtpsgen.jar
が LFS ポインターとして存在しますが、 .gitattributes
ファイルが存在しないことが分かります。 つまり、 Git LFS を使用しようとしたが正しくコミットされなかったために、壊れた状態であると考えられます。
このようなコミットは、古いバージョンの GitLab にはノーチェックで push することができましたが、最新の GitLab ではチェックされてリジェクトされるように変更されています。 そのため、外部のリポジトリを社内の GitLab サーバーに push すると今回のような問題に遭遇することがあるので注意が必要です。
最後に
GitLab はオープンソースソフトウェアなので、問題に遭遇した場合にソースコードから調査できることも大きな魅力です。 この記事が GitLab のソースコードを調査する際の参考になれば嬉しいです。