Commit 5f25cdfe authored by Drew Blessing's avatar Drew Blessing

Implement Merge Request Labels

parent 172ad962
...@@ -74,6 +74,10 @@ ...@@ -74,6 +74,10 @@
.merge-request-info { .merge-request-info {
color: #999; color: #999;
.merge-request-labels {
display: inline-block;
}
} }
} }
} }
...@@ -112,3 +116,7 @@ ...@@ -112,3 +116,7 @@
} }
} }
} }
.merge-request-show-labels .label {
padding: 6px 10px;
}
...@@ -117,6 +117,11 @@ class ApplicationController < ActionController::Base ...@@ -117,6 +117,11 @@ class ApplicationController < ActionController::Base
return access_denied! unless can?(current_user, :push_code, project) return access_denied! unless can?(current_user, :push_code, project)
end end
def authorize_labels!
# Labels should be accessible for issues and/or merge requests
authorize_read_issue! || authorize_read_merge_request!
end
def access_denied! def access_denied!
render "errors/access_denied", layout: "errors", status: 404 render "errors/access_denied", layout: "errors", status: 404
end end
......
class Projects::LabelsController < Projects::ApplicationController class Projects::LabelsController < Projects::ApplicationController
before_filter :module_enabled before_filter :module_enabled
# Allow read any issue before_filter :authorize_labels!
before_filter :authorize_read_issue!
respond_to :js, :html respond_to :js, :html
...@@ -13,12 +12,18 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -13,12 +12,18 @@ class Projects::LabelsController < Projects::ApplicationController
def generate def generate
Gitlab::IssuesLabels.generate(@project) Gitlab::IssuesLabels.generate(@project)
if params[:redirect] == 'issues'
redirect_to project_issues_path(@project) redirect_to project_issues_path(@project)
elsif params[:redirect] == 'merge_requests'
redirect_to project_merge_requests_path(@project)
end
end end
protected protected
def module_enabled def module_enabled
return render_404 unless @project.issues_enabled unless @project.issues_enabled || @project.merge_requests_enabled
return render_404
end
end end
end end
...@@ -36,7 +36,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -36,7 +36,9 @@ class MergeRequest < ActiveRecord::Base
delegate :commits, :diffs, :last_commit, :last_commit_short_sha, to: :merge_request_diff, prefix: nil delegate :commits, :diffs, :last_commit, :last_commit_short_sha, to: :merge_request_diff, prefix: nil
attr_accessible :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, :state_event, :description attr_accessible :title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
:state_event, :description, :label_list
attr_accessor :should_remove_source_branch attr_accessor :should_remove_source_branch
...@@ -44,6 +46,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -44,6 +46,9 @@ class MergeRequest < ActiveRecord::Base
# It allows us to close or modify broken merge requests # It allows us to close or modify broken merge requests
attr_accessor :allow_broken attr_accessor :allow_broken
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :labels
state_machine :state, initial: :opened do state_machine :state, initial: :opened do
event :close do event :close do
transition [:reopened, :opened] => :closed transition [:reopened, :opened] => :closed
......
...@@ -281,8 +281,11 @@ class Project < ActiveRecord::Base ...@@ -281,8 +281,11 @@ class Project < ActiveRecord::Base
self.id self.id
end end
# Tags are shared by issues and merge requests
def issues_labels def issues_labels
@issues_labels ||= (issues_default_labels + issues.tags_on(:labels)).uniq.sort_by(&:name) @issues_labels ||= (issues_default_labels +
merge_requests.tags_on(:labels) +
issues.tags_on(:labels)).uniq.sort_by(&:name)
end end
def issue_exists?(issue_id) def issue_exists?(issue_id)
......
= render "head" = render "head"
.row .row
.col-md-3 .col-md-3
= render 'shared/project_filter', project_entities_path: project_issues_path(@project), labels: true = render 'shared/project_filter', project_entities_path: project_issues_path(@project),
labels: true, redirect: 'issues'
.col-md-9.issues-holder .col-md-9.issues-holder
= render "issues" = render "issues"
...@@ -52,6 +52,15 @@ ...@@ -52,6 +52,15 @@
= f.text_area :description, class: "form-control js-gfm-input", rows: 14 = f.text_area :description, class: "form-control js-gfm-input", rows: 14
%p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. %p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
- if @merge_request.persisted? # Only allow labels on edit to avoid fork vs upstream repo labels issue
.form-group
= f.label :label_list, class: 'control-label' do
%i.icon-tag
Labels
.col-sm-10
= f.text_field :label_list, maxlength: 2000, class: "form-control"
%p.hint Separate labels with commas.
.form-actions .form-actions
- if @merge_request.new_record? - if @merge_request.new_record?
= f.submit 'Submit merge request', class: "btn btn-create" = f.submit 'Submit merge request', class: "btn btn-create"
...@@ -83,3 +92,32 @@ ...@@ -83,3 +92,32 @@
target_branch.on("change", function() { target_branch.on("change", function() {
$.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: $(this).val() });
}); });
$("#merge_request_label_list")
.bind( "keydown", function( event ) {
if ( event.keyCode === $.ui.keyCode.TAB &&
$( this ).data( "autocomplete" ).menu.active ) {
event.preventDefault();
}
})
.bind("click", function(event) {
$(this).autocomplete("search", "");
})
.autocomplete({
minLength: 0,
source: function( request, response ) {
response( $.ui.autocomplete.filter(
#{raw labels_autocomplete_source}, extractLast( request.term ) ) );
},
focus: function() {
return false;
},
select: function(event, ui) {
var terms = split( this.value );
terms.pop();
terms.push( ui.item.value );
terms.push( "" );
this.value = terms.join( ", " );
return false;
}
});
...@@ -35,3 +35,9 @@ ...@@ -35,3 +35,9 @@
.pull-right .pull-right
%small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')} %small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')}
.merge-request-labels
- merge_request.labels.each do |label|
%span{class: "label #{label_css_class(label.name)}"}
%i.icon-tag
= label.name
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
= render "projects/merge_requests/show/mr_box" = render "projects/merge_requests/show/mr_box"
= render "projects/merge_requests/show/state_widget" = render "projects/merge_requests/show/state_widget"
= render "projects/merge_requests/show/commits" = render "projects/merge_requests/show/commits"
= render "projects/merge_requests/show/participants"
- if @commits.present? - if @commits.present?
%ul.nav.nav-tabs %ul.nav.nav-tabs
......
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
%hr %hr
.row .row
.col-md-3 .col-md-3
= render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project) = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project),
labels: true, redirect: 'merge_requests'
.col-md-9 .col-md-9
.mr-filters.append-bottom-10 .mr-filters.append-bottom-10
.dropdown.inline .dropdown.inline
......
.participants
%cite.cgray #{@merge_request.participants.count} participants
- @merge_request.participants.each do |participant|
= link_to_member(@project, participant, name: false, size: 24)
.merge-request-show-labels.pull-right
- @merge_request.labels.each do |label|
%span{class: "label #{label_css_class(label.name)}"}
%i.icon-tag
= label.name
&nbsp;
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
.light-well .light-well
Add first label to your issues Add first label to your issues
%br %br
or #{link_to 'generate', generate_project_labels_path(@project), method: :post} default set of labels or #{link_to 'generate', generate_project_labels_path(@project, redirect: redirect), method: :post} default set of labels
%fieldset %fieldset
- if %w(state scope milestone_id assignee_id label_name).select { |k| params[k].present? }.any? - if %w(state scope milestone_id assignee_id label_name).select { |k| params[k].present? }.any?
......
...@@ -135,6 +135,7 @@ module API ...@@ -135,6 +135,7 @@ module API
expose :target_branch, :source_branch, :upvotes, :downvotes expose :target_branch, :source_branch, :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id expose :source_project_id, :target_project_id
expose :label_list, as: :labels
end end
class SSHKey < Grape::Entity class SSHKey < Grape::Entity
......
...@@ -67,6 +67,7 @@ module API ...@@ -67,6 +67,7 @@ module API
# assignee_id - Assignee user ID # assignee_id - Assignee user ID
# title (required) - Title of MR # title (required) - Title of MR
# description - Description of MR # description - Description of MR
# labels (optional) - Labels for MR as a comma-separated list
# #
# Example: # Example:
# POST /projects/:id/merge_requests # POST /projects/:id/merge_requests
...@@ -75,6 +76,7 @@ module API ...@@ -75,6 +76,7 @@ module API
authorize! :write_merge_request, user_project authorize! :write_merge_request, user_project
required_attributes! [:source_branch, :target_branch, :title] required_attributes! [:source_branch, :target_branch, :title]
attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description] attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description]
attrs[:label_list] = params[:labels] if params[:labels].present?
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute
if merge_request.valid? if merge_request.valid?
...@@ -95,11 +97,13 @@ module API ...@@ -95,11 +97,13 @@ module API
# title - Title of MR # title - Title of MR
# state_event - Status of MR. (close|reopen|merge) # state_event - Status of MR. (close|reopen|merge)
# description - Description of MR # description - Description of MR
# labels (optional) - Labels for a MR as a comma-separated list
# Example: # Example:
# PUT /projects/:id/merge_request/:merge_request_id # PUT /projects/:id/merge_request/:merge_request_id
# #
put ":id/merge_request/:merge_request_id" do put ":id/merge_request/:merge_request_id" do
attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event, :description] attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event, :description]
attrs[:label_list] = params[:labels] if params[:labels].present?
merge_request = user_project.merge_requests.find(params[:merge_request_id]) merge_request = user_project.merge_requests.find(params[:merge_request_id])
authorize! :modify_merge_request, merge_request authorize! :modify_merge_request, merge_request
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request) merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
......
...@@ -14,6 +14,12 @@ describe API::API, api: true do ...@@ -14,6 +14,12 @@ describe API::API, api: true do
let(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) } let(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) }
let(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) } let(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) }
let(:issue_with_labels) { create(:issue, author: user, assignee: user, project: project, :label_list => "label1, label2") } let(:issue_with_labels) { create(:issue, author: user, assignee: user, project: project, :label_list => "label1, label2") }
let(:merge_request_with_labels) do
create(:merge_request, :simple, author: user, assignee: user,
source_project: project, target_project: project, title: 'Test',
label_list: 'label3, label4')
end
describe "GET /projects" do describe "GET /projects" do
before { project } before { project }
...@@ -634,10 +640,11 @@ describe API::API, api: true do ...@@ -634,10 +640,11 @@ describe API::API, api: true do
end end
end end
describe "GET /projects/:id/labels" do describe 'GET /projects/:id/labels' do
context 'with an issue' do
before { issue_with_labels } before { issue_with_labels }
it "should return project labels" do it 'should return project labels' do
get api("/projects/#{project.id}/labels", user) get api("/projects/#{project.id}/labels", user)
response.status.should == 200 response.status.should == 200
json_response.should be_an Array json_response.should be_an Array
...@@ -645,4 +652,33 @@ describe API::API, api: true do ...@@ -645,4 +652,33 @@ describe API::API, api: true do
json_response.last['name'].should == issue_with_labels.labels.last.name json_response.last['name'].should == issue_with_labels.labels.last.name
end end
end end
context 'with a merge request' do
before { merge_request_with_labels }
it 'should return project labels' do
get api("/projects/#{project.id}/labels", user)
response.status.should == 200
json_response.should be_an Array
json_response.first['name'].should == merge_request_with_labels.labels.first.name
json_response.last['name'].should == merge_request_with_labels.labels.last.name
end
end
context 'with an issue and a merge request' do
before do
issue_with_labels
merge_request_with_labels
end
it 'should return project labels from both' do
get api("/projects/#{project.id}/labels", user)
response.status.should == 200
json_response.should be_an Array
all_labels = issue_with_labels.labels.map(&:name).to_a
.concat(merge_request_with_labels.labels.map(&:name).to_a)
json_response.map { |e| e['name'] }.should =~ all_labels
end
end
end
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