diff options
author | Bob Van Landuyt <bob@vanlanduyt.co> | 2018-10-24 18:01:44 +0200 |
---|---|---|
committer | Bob Van Landuyt <bob@vanlanduyt.co> | 2018-11-07 16:27:55 +0100 |
commit | 6fbdc5ed5224154b89cf351e11a8f9db48e6d7f0 (patch) | |
tree | dd2ccecd4b1100d4b83f91292b96ec988eecde6d /lib | |
parent | 6d8810a64f944ff96af54e5c759f866bb68a7453 (diff) | |
download | gitlab-ce-6fbdc5ed5224154b89cf351e11a8f9db48e6d7f0.tar.gz |
Apply patches when creating MR via email
This allows users to add patches as attachments to merge request
created via email.
When an email to create a merge request is sent, all the attachments
ending in `.patch` will be applied to the branch specified in the
subject of the email. If the branch did not exist, it will be created
from the HEAD of the repository.
When the patches could not be applied, the error message will be
replied to the user.
The patches can have a maximum combined size of 2MB for now.
Diffstat (limited to 'lib')
-rw-r--r-- | lib/gitlab/email/handler/create_merge_request_handler.rb | 50 | ||||
-rw-r--r-- | lib/gitlab/email/receiver.rb | 1 | ||||
-rw-r--r-- | lib/gitlab/git/patches/collection.rb | 33 | ||||
-rw-r--r-- | lib/gitlab/git/patches/commit_patches.rb | 31 | ||||
-rw-r--r-- | lib/gitlab/git/patches/patch.rb | 19 | ||||
-rw-r--r-- | lib/gitlab/gitaly_client/operation_service.rb | 23 |
6 files changed, 155 insertions, 2 deletions
diff --git a/lib/gitlab/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb index e68ae60ff98..5772727e855 100644 --- a/lib/gitlab/email/handler/create_merge_request_handler.rb +++ b/lib/gitlab/email/handler/create_merge_request_handler.rb @@ -44,10 +44,26 @@ module Gitlab @project ||= Project.find_by_full_path(project_path) end + def metrics_params + super.merge(includes_patches: patch_attachments.any?) + end + private + def build_merge_request + MergeRequests::BuildService.new(project, author, merge_request_params).execute + end + def create_merge_request - merge_request = MergeRequests::BuildService.new(project, author, merge_request_params).execute + merge_request = build_merge_request + + if patch_attachments.any? + apply_patches_to_source_branch(start_branch: merge_request.target_branch) + remove_patch_attachments + # Rebuild the merge request as the source branch might just have + # been created, so we should re-validate. + merge_request = build_merge_request + end if merge_request.errors.any? merge_request @@ -59,12 +75,42 @@ module Gitlab def merge_request_params params = { source_project_id: project.id, - source_branch: mail.subject, + source_branch: source_branch, target_project_id: project.id } params[:description] = message if message.present? params end + + def apply_patches_to_source_branch(start_branch:) + patches = patch_attachments.map { |patch| patch.body.decoded } + + result = Commits::CommitPatchService + .new(project, author, branch_name: source_branch, patches: patches, start_branch: start_branch) + .execute + + if result[:status] != :success + message = "Could not apply patches to #{source_branch}:\n#{result[:message]}" + raise InvalidAttachment, message + end + end + + def remove_patch_attachments + patch_attachments.each { |patch| mail.parts.delete(patch) } + # reset the message, so it needs to be reporocessed when the attachments + # have been modified + @message = nil + end + + def patch_attachments + @patches ||= mail.attachments + .select { |attachment| attachment.filename.ends_with?('.patch') } + .sort_by(&:filename) + end + + def source_branch + @source_branch ||= mail.subject + end end end end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index d8c594ad0e7..3a689967a64 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -18,6 +18,7 @@ module Gitlab InvalidIssueError = Class.new(InvalidRecordError) InvalidMergeRequestError = Class.new(InvalidRecordError) UnknownIncomingEmail = Class.new(ProcessingError) + InvalidAttachment = Class.new(ProcessingError) class Receiver def initialize(raw) diff --git a/lib/gitlab/git/patches/collection.rb b/lib/gitlab/git/patches/collection.rb new file mode 100644 index 00000000000..ad6b5d32abc --- /dev/null +++ b/lib/gitlab/git/patches/collection.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Git + module Patches + class Collection + MAX_PATCH_SIZE = 2.megabytes + + def initialize(one_or_more_patches) + @patches = Array(one_or_more_patches).map do |patch_content| + Gitlab::Git::Patches::Patch.new(patch_content) + end + end + + def content + @patches.map(&:content).join("\n") + end + + def valid_size? + size < MAX_PATCH_SIZE + end + + # rubocop: disable CodeReuse/ActiveRecord + # `@patches` is not an `ActiveRecord` relation, but an `Enumerable` + # We're using sum from `ActiveSupport` + def size + @size ||= @patches.sum(&:size) + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + end +end diff --git a/lib/gitlab/git/patches/commit_patches.rb b/lib/gitlab/git/patches/commit_patches.rb new file mode 100644 index 00000000000..c62994432d3 --- /dev/null +++ b/lib/gitlab/git/patches/commit_patches.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Git + module Patches + class CommitPatches + include Gitlab::Git::WrapsGitalyErrors + + def initialize(user, repository, branch, patch_collection) + @user, @repository, @branch, @patches = user, repository, branch, patch_collection + end + + def commit + repository.with_cache_hooks do + wrapped_gitaly_errors do + operation_service.user_commit_patches(user, branch, patches.content) + end + end + end + + private + + attr_reader :user, :repository, :branch, :patches + + def operation_service + repository.raw.gitaly_operation_client + end + end + end + end +end diff --git a/lib/gitlab/git/patches/patch.rb b/lib/gitlab/git/patches/patch.rb new file mode 100644 index 00000000000..fe6ae1b5b00 --- /dev/null +++ b/lib/gitlab/git/patches/patch.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Git + module Patches + class Patch + attr_reader :content + + def initialize(content) + @content = content + end + + def size + content.bytesize + end + end + end + end +end diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 1f42f657f68..4c78b790ce5 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -299,6 +299,29 @@ module Gitlab Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update) end + def user_commit_patches(user, branch_name, patches) + header = Gitaly::UserApplyPatchRequest::Header.new( + repository: @gitaly_repo, + user: Gitlab::Git::User.from_gitlab(user).to_gitaly, + target_branch: encode_binary(branch_name) + ) + reader = binary_stringio(patches) + + chunks = Enumerator.new do |chunk| + chunk.yield Gitaly::UserApplyPatchRequest.new(header: header) + + until reader.eof? + patch_chunk = reader.read(MAX_MSG_SIZE) + + chunk.yield(Gitaly::UserApplyPatchRequest.new(patches: patch_chunk)) + end + end + + response = GitalyClient.call(@repository.storage, :operation_service, :user_apply_patch, chunks) + + Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update) + end + private def call_cherry_pick_or_revert(rpc, user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) |