5a3e6fdff9
This commit, introduced in https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23812, fixes a problem creating a displaying image diff notes when the image is stored in LFS. The main problem was that `Gitlab::Diff::File` was returning an invalid valid in `text?` for this kind of files. It also fixes a rendering problem with other LFS files, like text ones. They LFS pointer shouldn't be shown when LFS is enabled for the project, but they were.
181 lines
5.8 KiB
Ruby
181 lines
5.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module Git
|
|
class Blob
|
|
include Gitlab::BlobHelper
|
|
include Gitlab::EncodingHelper
|
|
extend Gitlab::Git::WrapsGitalyErrors
|
|
|
|
# This number is the maximum amount of data that we want to display to
|
|
# the user. We load as much as we can for encoding detection and LFS
|
|
# pointer parsing. All other cases where we need full blob data should
|
|
# use load_all_data!.
|
|
MAX_DATA_DISPLAY_SIZE = 10.megabytes
|
|
|
|
# These limits are used as a heuristic to ignore files which can't be LFS
|
|
# pointers. The format of these is described in
|
|
# https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md#the-pointer
|
|
LFS_POINTER_MIN_SIZE = 120.bytes
|
|
LFS_POINTER_MAX_SIZE = 200.bytes
|
|
|
|
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
|
|
|
|
class << self
|
|
def find(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
|
|
return unless path
|
|
|
|
path = path.sub(%r{\A/*}, '')
|
|
path = '/' if path.empty?
|
|
name = File.basename(path)
|
|
|
|
# Gitaly will think that setting the limit to 0 means unlimited, while
|
|
# the client might only need the metadata and thus set the limit to 0.
|
|
# In this method we'll then set the limit to 1, but clear the byte of data
|
|
# that we got back so for the outside world it looks like the limit was
|
|
# actually 0.
|
|
req_limit = limit == 0 ? 1 : limit
|
|
|
|
entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, req_limit)
|
|
return unless entry
|
|
|
|
entry.data = "" if limit == 0
|
|
|
|
case entry.type
|
|
when :COMMIT
|
|
new(id: entry.oid, name: name, size: 0, data: '', path: path, commit_id: sha)
|
|
when :BLOB
|
|
new(id: entry.oid, name: name, size: entry.size, data: entry.data.dup, mode: entry.mode.to_s(8),
|
|
path: path, commit_id: sha, binary: binary?(entry.data))
|
|
end
|
|
end
|
|
|
|
def raw(repository, sha)
|
|
repository.gitaly_blob_client.get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
|
|
end
|
|
|
|
# Returns an array of Blob instances, specified in blob_references as
|
|
# [[commit_sha, path], [commit_sha, path], ...]. If blob_size_limit < 0 then the
|
|
# full blob contents are returned. If blob_size_limit >= 0 then each blob will
|
|
# contain no more than limit bytes in its data attribute.
|
|
#
|
|
# Keep in mind that this method may allocate a lot of memory. It is up
|
|
# to the caller to limit the number of blobs and blob_size_limit.
|
|
#
|
|
def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE)
|
|
repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a
|
|
end
|
|
|
|
# Returns an array of Blob instances just with the metadata, that means
|
|
# the data attribute has no content.
|
|
def batch_metadata(repository, blob_references)
|
|
batch(repository, blob_references, blob_size_limit: 0)
|
|
end
|
|
|
|
# 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)
|
|
wrapped_gitaly_errors do
|
|
repository.gitaly_blob_client.batch_lfs_pointers(blob_ids.to_a)
|
|
end
|
|
end
|
|
|
|
def binary?(data)
|
|
EncodingHelper.detect_libgit2_binary?(data)
|
|
end
|
|
|
|
def size_could_be_lfs?(size)
|
|
size.between?(LFS_POINTER_MIN_SIZE, LFS_POINTER_MAX_SIZE)
|
|
end
|
|
end
|
|
|
|
def initialize(options)
|
|
%w(id name path size data mode commit_id binary).each do |key|
|
|
self.__send__("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend
|
|
end
|
|
|
|
# Retain the actual size before it is encoded
|
|
@loaded_size = @data.bytesize if @data
|
|
@loaded_all_data = @loaded_size == size
|
|
end
|
|
|
|
def binary_in_repo?
|
|
@binary.nil? ? super : @binary == true
|
|
end
|
|
|
|
def data
|
|
encode! @data
|
|
end
|
|
|
|
# Load all blob data (not just the first MAX_DATA_DISPLAY_SIZE bytes) into
|
|
# memory as a Ruby string.
|
|
def load_all_data!(repository)
|
|
return if @data == '' # don't mess with submodule blobs
|
|
|
|
# Even if we return early, recalculate wether this blob is binary in
|
|
# case a blob was initialized as text but the full data isn't
|
|
@binary = nil
|
|
|
|
return if @loaded_all_data
|
|
|
|
@data = repository.gitaly_blob_client.get_blob(oid: id, limit: -1).data
|
|
@loaded_all_data = true
|
|
@loaded_size = @data.bytesize
|
|
end
|
|
|
|
def name
|
|
encode! @name
|
|
end
|
|
|
|
def path
|
|
encode! @path
|
|
end
|
|
|
|
def truncated?
|
|
size && (size > loaded_size)
|
|
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
|
|
|
|
def external_storage
|
|
return unless lfs_pointer?
|
|
|
|
:lfs
|
|
end
|
|
|
|
alias_method :external_size, :lfs_size
|
|
|
|
private
|
|
|
|
def has_lfs_version_key?
|
|
!empty? && text_in_repo? && data.start_with?("version https://git-lfs.github.com/spec")
|
|
end
|
|
end
|
|
end
|
|
end
|