Merge pull request #6778 from dblessing/feature/mr_labels
Merge Request Labels
This commit is contained in:
commit
1f1c59b61d
15 changed files with 142 additions and 17 deletions
|
@ -73,6 +73,10 @@
|
||||||
|
|
||||||
.merge-request-info {
|
.merge-request-info {
|
||||||
color: #999;
|
color: #999;
|
||||||
|
|
||||||
|
.merge-request-labels {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,3 +115,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.merge-request-show-labels .label {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
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
|
||||||
def generate
|
def generate
|
||||||
Gitlab::IssuesLabels.generate(@project)
|
Gitlab::IssuesLabels.generate(@project)
|
||||||
|
|
||||||
redirect_to project_issues_path(@project)
|
if params[:redirect] == 'issues'
|
||||||
|
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
|
||||||
|
|
||||||
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
|
||||||
# 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
|
||||||
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)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
= 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"
|
||||||
|
|
|
@ -42,6 +42,15 @@
|
||||||
.col-sm-10= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2'})
|
.col-sm-10= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2'})
|
||||||
|
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
@ -60,3 +69,32 @@
|
||||||
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
|
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -31,3 +31,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 @@
|
||||||
= 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 @@
|
||||||
%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
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
.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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -136,6 +136,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
|
||||||
# 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
|
||||||
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
|
||||||
# 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
|
||||||
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,15 +640,45 @@ describe API::API, api: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /projects/:id/labels" do
|
describe 'GET /projects/:id/labels' do
|
||||||
before { issue_with_labels }
|
context 'with an issue' do
|
||||||
|
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
|
||||||
json_response.first['name'].should == issue_with_labels.labels.first.name
|
json_response.first['name'].should == issue_with_labels.labels.first.name
|
||||||
json_response.last['name'].should == issue_with_labels.labels.last.name
|
json_response.last['name'].should == issue_with_labels.labels.last.name
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue