2020年06月18日 - Hiroyuki Sato    

「LFS objects are missing」でpushできない時の回避策

GitLabにpushすると「LFS objects are missing」エラーが発生する場合があります。その回避策と原因について説明します。

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_checktrue の場合

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 のソースコードを調査する際の参考になれば嬉しいです。

Gitlab x icon svg