Commit 1f92f2cc authored by Jan Provaznik's avatar Jan Provaznik Committed by Jan Provaznik
Browse files

Create merge request from email

* new merge request can be created by sending an email to the specific
email address (similar to creating issues by email)
* for the first iteration, source branch must be specified in the mail
subject, other merge request parameters can not be set yet
* user should enable "Receive notifications about your own activity" in
user settings to receive a notification about created merge request

Part of #32878
parent e40527cd7b17
......@@ -28,7 +28,7 @@ export default class IssuableIndex {
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success(response) {
$('#issue_email').val(response.new_issue_address).focus();
$('#issuable_email').val(response.new_address).focus();
},
beforeSend() {
$('.incoming-email-token-reset').text('resetting...');
......
......@@ -164,12 +164,7 @@ ul.related-merge-requests > li {
}
}
.issues-footer {
padding-top: $gl-padding;
padding-bottom: 37px;
}
.issue-email-modal-btn {
.issuable-email-modal-btn {
padding: 0;
color: $gl-link-color;
background-color: transparent;
......
......@@ -1210,3 +1210,8 @@ pre.light-well {
border-color: $border-color;
}
}
.issuable-footer {
padding-top: $gl-padding;
padding-bottom: 37px;
}
......@@ -133,11 +133,11 @@ def destroy
redirect_to edit_project_path(@project), status: 302, alert: ex.message
end
def new_issue_address
def new_issuable_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
current_user.reset_incoming_email_token!
render json: { new_issue_address: @project.new_issue_address(current_user) }
render json: { new_address: @project.new_issuable_address(current_user, params[:issuable_type]) }
end
def archive
......
......@@ -752,13 +752,14 @@ def web_url
Gitlab::Routing.url_helpers.project_url(self)
end
def new_issue_address(author)
def new_issuable_address(author, address_type)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
author.ensure_incoming_email_token!
suffix = address_type == 'merge_request' ? '+merge-request' : ''
Gitlab::IncomingEmail.reply_address(
"#{full_path}+#{author.incoming_email_token}")
"#{full_path}#{suffix}+#{author.incoming_email_token}")
end
def build_commit_note(commit)
......
......@@ -10,8 +10,12 @@ def execute
merge_request.target_branch = find_target_branch
merge_request.can_be_created = branches_valid?
compare_branches if branches_present?
assign_title_and_description if merge_request.can_be_created
# compare branches only if branches are valid, otherwise
# compare_branches may raise an error
if merge_request.can_be_created
compare_branches
assign_title_and_description
end
merge_request
end
......
......@@ -35,6 +35,12 @@ def after_create(issuable)
super
end
# expose issuable create method so it can be called from email
# handler CreateMergeRequestHandler
def create(merge_request)
super
end
private
def update_merge_requests_head_pipeline(merge_request)
......
.issues-footer.text-center
%button.issue-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issue-email-modal" } }
Email a new issue to this project
- name = issuable_type == 'issue' ? 'issue' : 'merge request'
#issue-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
.issuable-footer.text-center
%button.issuable-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issuable-email-modal" } }
Email a new #{name} to this project
#issuable-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
.modal-dialog{ role: "document" }
.modal-content
.modal-header
%button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
%span{ aria: { hidden: "true" } }= icon("times")
%h4.modal-title
Create new issue by email
Create new #{name} by email
.modal-body
%p
You can create a new issue inside this project by sending an email to the following email address:
You can create a new #{name} inside this project by sending an email to the following email address:
.email-modal-input-group.input-group
= text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-btn
= clipboard_button(target: '#issue_email')
= clipboard_button(target: '#issuable_email')
%p
The subject will be used as the title of the new issue, and the message will be the description.
= link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
and styling with
= link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
are supported.
= render 'by_email_description'
%p
This is a private email address, generated just for you.
Anyone who gets ahold of it can create issues as if they were you.
Anyone who gets ahold of it can create issues or merge requests as if they were you.
You should
= link_to 'reset it', new_issue_address_project_path(@project), class: 'incoming-email-token-reset'
= link_to 'reset it', new_issuable_address_project_path(@project, issuable_type: issuable_type), class: 'incoming-email-token-reset'
if that ever happens.
The subject will be used as the title of the new issue, and the message will be the description.
= link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
and styling with
= link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
are supported.
......@@ -2,7 +2,7 @@
- @can_bulk_update = can?(current_user, :admin_issue, @project)
- page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user)
- new_issue_email = @project.new_issuable_address(current_user, 'issue')
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
......@@ -25,6 +25,6 @@
.issues-holder
= render 'issues'
- if new_issue_email
= render 'issue_by_email', email: new_issue_email
= render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue'
- else
= render 'shared/empty_states/issues', button_path: new_project_issue_path(@project)
The subject will be used as the source branch name for the new merge request and the target branch will be the default branch for the project.
......@@ -4,6 +4,7 @@
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests"
- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
......@@ -25,5 +26,7 @@
.merge-requests-holder
= render 'merge_requests'
- if new_merge_request_email
= render 'projects/issuable_by_email', email: new_merge_request_email, issuable_type: 'merge_request'
- else
= render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
......@@ -39,8 +39,7 @@ def handle_failure(raw, e)
"You are not allowed to perform this action. If you believe this is in error, contact a staff member."
when Gitlab::Email::NoteableNotFoundError
"The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
when Gitlab::Email::InvalidNoteError,
Gitlab::Email::InvalidIssueError
when Gitlab::Email::InvalidRecordError
can_retry = true
e.message
end
......
---
title: Allow creation of merge request from email
merge_request: 13817
author: janp
type: added
......@@ -429,7 +429,7 @@
get :download_export
get :activity
get :refs
put :new_issue_address
put :new_issuable_address
end
end
end
......
......@@ -27,7 +27,7 @@ With GitLab merge requests, you can:
- [Resolve merge conflicts from the UI](#resolve-conflicts)
- Enable [fast-forward merge requests](#fast-forward-merge-requests)
- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch
- [Create new merge requests by email](#create_by_email)
With **[GitLab Enterprise Edition][ee]**, you can also:
......@@ -132,6 +132,14 @@ those conflicts in the GitLab UI.
[Learn more about resolving merge conflicts in the UI.](resolve_conflicts.md)
## Create new merge requests by email
You can create a new merge request by sending an email to a user-specific email
address. The address can be obtained on the merge requests page by clicking on
a **Email a new merge request to this project** button. The subject will be
used as the source branch name for the new merge request and the target branch
will be the default branch for the project.
## Revert changes
GitLab implements Git's powerful feature to revert any commit with introducing
......
require 'gitlab/email/handler/create_merge_request_handler'
require 'gitlab/email/handler/create_note_handler'
require 'gitlab/email/handler/create_issue_handler'
require 'gitlab/email/handler/unsubscribe_handler'
......@@ -8,6 +9,7 @@ module Handler
HANDLERS = [
UnsubscribeHandler,
CreateNoteHandler,
CreateMergeRequestHandler,
CreateIssueHandler
].freeze
......
require 'gitlab/email/handler/base_handler'
require 'gitlab/email/handler/reply_processing'
module Gitlab
module Email
module Handler
class CreateMergeRequestHandler < BaseHandler
include ReplyProcessing
attr_reader :project_path, :incoming_email_token
def initialize(mail, mail_key)
super(mail, mail_key)
if m = /\A([^\+]*)\+merge-request\+(.*)/.match(mail_key.to_s)
@project_path, @incoming_email_token = m.captures
end
end
def can_handle?
@project_path && @incoming_email_token
end
def execute
raise ProjectNotFound unless project
validate_permission!(:create_merge_request)
verify_record!(
record: create_merge_request,
invalid_exception: InvalidMergeRequestError,
record_name: 'merge_request')
end
def author
@author ||= User.find_by(incoming_email_token: incoming_email_token)
end
def project
@project ||= Project.find_by_full_path(project_path)
end
def metrics_params
super.merge(project: project&.full_path)
end
private
def create_merge_request
merge_request = MergeRequests::BuildService.new(project, author, merge_request_params).execute
if merge_request.errors.any?
merge_request
else
MergeRequests::CreateService.new(project, author).create(merge_request)
end
end
def merge_request_params
{
source_project_id: project.id,
source_branch: mail.subject,
target_project_id: project.id
}
end
end
end
end
end
......@@ -13,8 +13,10 @@ module Email
UserBlockedError = Class.new(ProcessingError)
UserNotAuthorizedError = Class.new(ProcessingError)
NoteableNotFoundError = Class.new(ProcessingError)
InvalidNoteError = Class.new(ProcessingError)
InvalidIssueError = Class.new(ProcessingError)
InvalidRecordError = Class.new(ProcessingError)
InvalidNoteError = Class.new(InvalidRecordError)
InvalidIssueError = Class.new(InvalidRecordError)
InvalidMergeRequestError = Class.new(InvalidRecordError)
UnknownIncomingEmail = Class.new(ProcessingError)
class Receiver
......
......@@ -405,11 +405,12 @@ def update_project(**parameters)
end
end
describe 'PUT #new_issue_address' do
describe 'PUT #new_issuable_address for issue' do
subject do
put :new_issue_address,
put :new_issuable_address,
namespace_id: project.namespace,
id: project
id: project,
issuable_type: 'issue'
user.reload
end
......@@ -428,7 +429,35 @@ def update_project(**parameters)
end
it 'changes projects new issue address' do
expect { subject }.to change { project.new_issue_address(user) }
expect { subject }.to change { project.new_issuable_address(user, 'issue') }
end
end
describe 'PUT #new_issuable_address for merge request' do
subject do
put :new_issuable_address,
namespace_id: project.namespace,
id: project,
issuable_type: 'merge_request'
user.reload
end
before do
sign_in(user)
project.team << [user, :developer]
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
end
it 'has http status 200' do
expect(response).to have_http_status(200)
end
it 'changes the user incoming email token' do
expect { subject }.to change { user.incoming_email_token }
end
it 'changes projects new merge request address' do
expect { subject }.to change { project.new_issuable_address(user, 'merge_request') }
end
end
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment