2020-02-13 15:08:52 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
class SnippetRepository < ApplicationRecord
|
|
|
|
include Shardable
|
|
|
|
|
2020-02-28 18:09:07 +00:00
|
|
|
DEFAULT_EMPTY_FILE_NAME = 'snippetfile'
|
2020-03-10 12:08:16 +00:00
|
|
|
EMPTY_FILE_PATTERN = /^#{DEFAULT_EMPTY_FILE_NAME}(\d+)\.txt$/.freeze
|
2020-02-28 18:09:07 +00:00
|
|
|
|
|
|
|
CommitError = Class.new(StandardError)
|
2020-05-06 21:10:00 +00:00
|
|
|
InvalidPathError = Class.new(CommitError)
|
2020-05-11 21:09:40 +00:00
|
|
|
InvalidSignatureError = Class.new(CommitError)
|
2020-02-28 18:09:07 +00:00
|
|
|
|
2020-02-13 15:08:52 +00:00
|
|
|
belongs_to :snippet, inverse_of: :snippet_repository
|
|
|
|
|
2020-02-28 18:09:07 +00:00
|
|
|
delegate :repository, to: :snippet
|
|
|
|
|
2020-02-13 15:08:52 +00:00
|
|
|
class << self
|
|
|
|
def find_snippet(disk_path)
|
|
|
|
find_by(disk_path: disk_path)&.snippet
|
|
|
|
end
|
|
|
|
end
|
2020-02-28 18:09:07 +00:00
|
|
|
|
|
|
|
def multi_files_action(user, files = [], **options)
|
|
|
|
return if files.nil? || files.empty?
|
|
|
|
|
|
|
|
lease_key = "multi_files_action:#{snippet_id}"
|
|
|
|
|
|
|
|
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 120)
|
|
|
|
raise CommitError, 'Snippet is already being updated' unless uuid = lease.try_obtain
|
|
|
|
|
|
|
|
options[:actions] = transform_file_entries(files)
|
|
|
|
|
|
|
|
capture_git_error { repository.multi_action(user, **options) }
|
|
|
|
ensure
|
|
|
|
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def capture_git_error(&block)
|
|
|
|
yield block
|
|
|
|
rescue Gitlab::Git::Index::IndexError,
|
|
|
|
Gitlab::Git::CommitError,
|
|
|
|
Gitlab::Git::PreReceiveError,
|
2020-05-11 21:09:40 +00:00
|
|
|
Gitlab::Git::CommandError,
|
|
|
|
ArgumentError => error
|
2020-05-13 21:08:55 +00:00
|
|
|
|
|
|
|
logger.error(message: "Snippet git error. Reason: #{error.message}", snippet: snippet.id)
|
|
|
|
|
2020-05-06 21:10:00 +00:00
|
|
|
raise commit_error_exception(error)
|
2020-02-28 18:09:07 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def transform_file_entries(files)
|
2020-03-10 12:08:16 +00:00
|
|
|
next_index = get_last_empty_file_index + 1
|
2020-02-28 18:09:07 +00:00
|
|
|
|
2020-05-29 12:08:19 +00:00
|
|
|
files.map do |file_entry|
|
2020-03-23 12:09:47 +00:00
|
|
|
file_entry[:file_path] = file_path_for(file_entry, next_index) { next_index += 1 }
|
2020-02-28 18:09:07 +00:00
|
|
|
file_entry[:action] = infer_action(file_entry) unless file_entry[:action]
|
2020-05-29 12:08:19 +00:00
|
|
|
file_entry[:action] = file_entry[:action].to_sym
|
|
|
|
|
|
|
|
if only_rename_action?(file_entry)
|
|
|
|
file_entry[:infer_content] = true
|
|
|
|
elsif empty_update_action?(file_entry)
|
|
|
|
# There is no need to perform a repository operation
|
|
|
|
# When the update action has no content
|
|
|
|
file_entry = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
file_entry
|
|
|
|
end.compact
|
2020-02-28 18:09:07 +00:00
|
|
|
end
|
|
|
|
|
2020-03-23 12:09:47 +00:00
|
|
|
def file_path_for(file_entry, next_index)
|
|
|
|
return file_entry[:file_path] if file_entry[:file_path].present?
|
|
|
|
return file_entry[:previous_path] if reuse_previous_path?(file_entry)
|
|
|
|
|
|
|
|
build_empty_file_name(next_index).tap { yield }
|
|
|
|
end
|
|
|
|
|
|
|
|
# If the user removed the file_path and the previous_path
|
|
|
|
# matches the EMPTY_FILE_PATTERN, we don't need to
|
|
|
|
# rename the file and build a new empty file name,
|
|
|
|
# we can just reuse the existing file name
|
|
|
|
def reuse_previous_path?(file_entry)
|
|
|
|
file_entry[:file_path].blank? &&
|
|
|
|
EMPTY_FILE_PATTERN.match?(file_entry[:previous_path])
|
|
|
|
end
|
|
|
|
|
2020-02-28 18:09:07 +00:00
|
|
|
def infer_action(file_entry)
|
|
|
|
return :create if file_entry[:previous_path].blank?
|
|
|
|
|
|
|
|
file_entry[:previous_path] != file_entry[:file_path] ? :move : :update
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_last_empty_file_index
|
2020-03-10 12:08:16 +00:00
|
|
|
repository.ls_files(nil).inject(0) do |max, file|
|
|
|
|
idx = file[EMPTY_FILE_PATTERN, 1].to_i
|
|
|
|
[idx, max].max
|
|
|
|
end
|
2020-02-28 18:09:07 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def build_empty_file_name(index)
|
|
|
|
"#{DEFAULT_EMPTY_FILE_NAME}#{index}.txt"
|
|
|
|
end
|
2020-05-06 21:10:00 +00:00
|
|
|
|
2020-05-11 21:09:40 +00:00
|
|
|
def commit_error_exception(err)
|
|
|
|
if invalid_path_error?(err)
|
2020-05-13 21:08:55 +00:00
|
|
|
InvalidPathError.new('Invalid file name') # To avoid returning the message with the path included
|
2020-05-11 21:09:40 +00:00
|
|
|
elsif invalid_signature_error?(err)
|
|
|
|
InvalidSignatureError.new(err.message)
|
2020-05-06 21:10:00 +00:00
|
|
|
else
|
2020-05-11 21:09:40 +00:00
|
|
|
CommitError.new(err.message)
|
2020-05-06 21:10:00 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-11 21:09:40 +00:00
|
|
|
def invalid_path_error?(err)
|
|
|
|
err.is_a?(Gitlab::Git::Index::IndexError) &&
|
|
|
|
err.message.downcase.start_with?('invalid path', 'path cannot include directory traversal')
|
|
|
|
end
|
|
|
|
|
|
|
|
def invalid_signature_error?(err)
|
|
|
|
err.is_a?(ArgumentError) &&
|
|
|
|
err.message.downcase.match?(/failed to parse signature/)
|
2020-05-06 21:10:00 +00:00
|
|
|
end
|
2020-05-29 12:08:19 +00:00
|
|
|
|
|
|
|
def only_rename_action?(action)
|
|
|
|
action[:action] == :move && action[:content].nil?
|
|
|
|
end
|
|
|
|
|
|
|
|
def empty_update_action?(action)
|
|
|
|
action[:action] == :update && action[:content].nil?
|
|
|
|
end
|
2020-02-13 15:08:52 +00:00
|
|
|
end
|
2020-09-11 21:08:44 +00:00
|
|
|
|
|
|
|
SnippetRepository.prepend_if_ee('EE::SnippetRepository')
|