Commit 1f1c59b61d30b76d69f0f925b43a0b96465f38ed

Authored by Dmitriy Zaporozhets
2 parents 0c621708 5f25cdfe

Merge pull request #6778 from dblessing/feature/mr_labels

Merge Request Labels
app/assets/stylesheets/sections/merge_requests.scss
... ... @@ -73,6 +73,10 @@
73 73  
74 74 .merge-request-info {
75 75 color: #999;
  76 +
  77 + .merge-request-labels {
  78 + display: inline-block;
  79 + }
76 80 }
77 81 }
78 82 }
... ... @@ -111,3 +115,7 @@
111 115 }
112 116 }
113 117 }
  118 +
  119 +.merge-request-show-labels .label {
  120 + padding: 6px 10px;
  121 +}
... ...
app/controllers/application_controller.rb
... ... @@ -117,6 +117,11 @@ class ApplicationController < ActionController::Base
117 117 return access_denied! unless can?(current_user, :push_code, project)
118 118 end
119 119  
  120 + def authorize_labels!
  121 + # Labels should be accessible for issues and/or merge requests
  122 + authorize_read_issue! || authorize_read_merge_request!
  123 + end
  124 +
120 125 def access_denied!
121 126 render "errors/access_denied", layout: "errors", status: 404
122 127 end
... ...
app/controllers/projects/labels_controller.rb
1 1 class Projects::LabelsController < Projects::ApplicationController
2 2 before_filter :module_enabled
3 3  
4   - # Allow read any issue
5   - before_filter :authorize_read_issue!
  4 + before_filter :authorize_labels!
6 5  
7 6 respond_to :js, :html
8 7  
... ... @@ -13,12 +12,18 @@ class Projects::LabelsController &lt; Projects::ApplicationController
13 12 def generate
14 13 Gitlab::IssuesLabels.generate(@project)
15 14  
16   - redirect_to project_issues_path(@project)
  15 + if params[:redirect] == 'issues'
  16 + redirect_to project_issues_path(@project)
  17 + elsif params[:redirect] == 'merge_requests'
  18 + redirect_to project_merge_requests_path(@project)
  19 + end
17 20 end
18 21  
19 22 protected
20 23  
21 24 def module_enabled
22   - return render_404 unless @project.issues_enabled
  25 + unless @project.issues_enabled || @project.merge_requests_enabled
  26 + return render_404
  27 + end
23 28 end
24 29 end
... ...
app/models/merge_request.rb
... ... @@ -36,7 +36,9 @@ class MergeRequest &lt; ActiveRecord::Base
36 36  
37 37 delegate :commits, :diffs, :last_commit, :last_commit_short_sha, to: :merge_request_diff, prefix: nil
38 38  
39   - attr_accessible :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, :state_event, :description
  39 + attr_accessible :title, :assignee_id, :source_project_id, :source_branch,
  40 + :target_project_id, :target_branch, :milestone_id,
  41 + :state_event, :description, :label_list
40 42  
41 43 attr_accessor :should_remove_source_branch
42 44  
... ... @@ -44,6 +46,9 @@ class MergeRequest &lt; ActiveRecord::Base
44 46 # It allows us to close or modify broken merge requests
45 47 attr_accessor :allow_broken
46 48  
  49 + ActsAsTaggableOn.strict_case_match = true
  50 + acts_as_taggable_on :labels
  51 +
47 52 state_machine :state, initial: :opened do
48 53 event :close do
49 54 transition [:reopened, :opened] => :closed
... ...
app/models/project.rb
... ... @@ -281,8 +281,11 @@ class Project &lt; ActiveRecord::Base
281 281 self.id
282 282 end
283 283  
  284 + # Tags are shared by issues and merge requests
284 285 def issues_labels
285   - @issues_labels ||= (issues_default_labels + issues.tags_on(:labels)).uniq.sort_by(&:name)
  286 + @issues_labels ||= (issues_default_labels +
  287 + merge_requests.tags_on(:labels) +
  288 + issues.tags_on(:labels)).uniq.sort_by(&:name)
286 289 end
287 290  
288 291 def issue_exists?(issue_id)
... ...
app/views/projects/issues/index.html.haml
1 1 = render "head"
2 2 .row
3 3 .col-md-3
4   - = render 'shared/project_filter', project_entities_path: project_issues_path(@project), labels: true
  4 + = render 'shared/project_filter', project_entities_path: project_issues_path(@project),
  5 + labels: true, redirect: 'issues'
5 6 .col-md-9.issues-holder
6 7 = render "issues"
... ...
app/views/projects/merge_requests/_form.html.haml
... ... @@ -42,6 +42,15 @@
42 42 .col-sm-10= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2'})
43 43  
44 44  
  45 + - if @merge_request.persisted? # Only allow labels on edit to avoid fork vs upstream repo labels issue
  46 + .form-group
  47 + = f.label :label_list, class: 'control-label' do
  48 + %i.icon-tag
  49 + Labels
  50 + .col-sm-10
  51 + = f.text_field :label_list, maxlength: 2000, class: "form-control"
  52 + %p.hint Separate labels with commas.
  53 +
45 54 .form-actions
46 55 - if @merge_request.new_record?
47 56 = f.submit 'Submit merge request', class: "btn btn-create"
... ... @@ -60,3 +69,32 @@
60 69 $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
61 70 e.preventDefault();
62 71 });
  72 +
  73 + $("#merge_request_label_list")
  74 + .bind( "keydown", function( event ) {
  75 + if ( event.keyCode === $.ui.keyCode.TAB &&
  76 + $( this ).data( "autocomplete" ).menu.active ) {
  77 + event.preventDefault();
  78 + }
  79 + })
  80 + .bind("click", function(event) {
  81 + $(this).autocomplete("search", "");
  82 + })
  83 + .autocomplete({
  84 + minLength: 0,
  85 + source: function( request, response ) {
  86 + response( $.ui.autocomplete.filter(
  87 + #{raw labels_autocomplete_source}, extractLast( request.term ) ) );
  88 + },
  89 + focus: function() {
  90 + return false;
  91 + },
  92 + select: function(event, ui) {
  93 + var terms = split( this.value );
  94 + terms.pop();
  95 + terms.push( ui.item.value );
  96 + terms.push( "" );
  97 + this.value = terms.join( ", " );
  98 + return false;
  99 + }
  100 + });
... ...
app/views/projects/merge_requests/_merge_request.html.haml
... ... @@ -31,3 +31,9 @@
31 31  
32 32 .pull-right
33 33 %small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')}
  34 +
  35 + .merge-request-labels
  36 + - merge_request.labels.each do |label|
  37 + %span{class: "label #{label_css_class(label.name)}"}
  38 + %i.icon-tag
  39 + = label.name
... ...
app/views/projects/merge_requests/_show.html.haml
... ... @@ -4,6 +4,7 @@
4 4 = render "projects/merge_requests/show/mr_box"
5 5 = render "projects/merge_requests/show/state_widget"
6 6 = render "projects/merge_requests/show/commits"
  7 + = render "projects/merge_requests/show/participants"
7 8  
8 9 - if @commits.present?
9 10 %ul.nav.nav-tabs
... ...
app/views/projects/merge_requests/index.html.haml
... ... @@ -8,7 +8,8 @@
8 8 %hr
9 9 .row
10 10 .col-md-3
11   - = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project)
  11 + = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project),
  12 + labels: true, redirect: 'merge_requests'
12 13 .col-md-9
13 14 .mr-filters.append-bottom-10
14 15 .dropdown.inline
... ...
app/views/projects/merge_requests/show/_participants.html.haml 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +.participants
  2 + %cite.cgray #{@merge_request.participants.count} participants
  3 + - @merge_request.participants.each do |participant|
  4 + = link_to_member(@project, participant, name: false, size: 24)
  5 +
  6 + .merge-request-show-labels.pull-right
  7 + - @merge_request.labels.each do |label|
  8 + %span{class: "label #{label_css_class(label.name)}"}
  9 + %i.icon-tag
  10 + = label.name
  11 + &nbsp;
... ...
app/views/shared/_project_filter.html.haml
... ... @@ -44,7 +44,7 @@
44 44 .light-well
45 45 Add first label to your issues
46 46 %br
47   - or #{link_to 'generate', generate_project_labels_path(@project), method: :post} default set of labels
  47 + or #{link_to 'generate', generate_project_labels_path(@project, redirect: redirect), method: :post} default set of labels
48 48  
49 49 %fieldset
50 50 - if %w(state scope milestone_id assignee_id label_name).select { |k| params[k].present? }.any?
... ...
lib/api/entities.rb
... ... @@ -136,6 +136,7 @@ module API
136 136 expose :target_branch, :source_branch, :upvotes, :downvotes
137 137 expose :author, :assignee, using: Entities::UserBasic
138 138 expose :source_project_id, :target_project_id
  139 + expose :label_list, as: :labels
139 140 end
140 141  
141 142 class SSHKey < Grape::Entity
... ...
lib/api/merge_requests.rb
... ... @@ -67,6 +67,7 @@ module API
67 67 # assignee_id - Assignee user ID
68 68 # title (required) - Title of MR
69 69 # description - Description of MR
  70 + # labels (optional) - Labels for MR as a comma-separated list
70 71 #
71 72 # Example:
72 73 # POST /projects/:id/merge_requests
... ... @@ -75,6 +76,7 @@ module API
75 76 authorize! :write_merge_request, user_project
76 77 required_attributes! [:source_branch, :target_branch, :title]
77 78 attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description]
  79 + attrs[:label_list] = params[:labels] if params[:labels].present?
78 80 merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute
79 81  
80 82 if merge_request.valid?
... ... @@ -95,11 +97,13 @@ module API
95 97 # title - Title of MR
96 98 # state_event - Status of MR. (close|reopen|merge)
97 99 # description - Description of MR
  100 + # labels (optional) - Labels for a MR as a comma-separated list
98 101 # Example:
99 102 # PUT /projects/:id/merge_request/:merge_request_id
100 103 #
101 104 put ":id/merge_request/:merge_request_id" do
102 105 attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event, :description]
  106 + attrs[:label_list] = params[:labels] if params[:labels].present?
103 107 merge_request = user_project.merge_requests.find(params[:merge_request_id])
104 108 authorize! :modify_merge_request, merge_request
105 109 merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
... ...
spec/requests/api/projects_spec.rb
... ... @@ -14,6 +14,12 @@ describe API::API, api: true do
14 14 let(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) }
15 15 let(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) }
16 16 let(:issue_with_labels) { create(:issue, author: user, assignee: user, project: project, :label_list => "label1, label2") }
  17 + let(:merge_request_with_labels) do
  18 + create(:merge_request, :simple, author: user, assignee: user,
  19 + source_project: project, target_project: project, title: 'Test',
  20 + label_list: 'label3, label4')
  21 + end
  22 +
17 23  
18 24 describe "GET /projects" do
19 25 before { project }
... ... @@ -634,15 +640,45 @@ describe API::API, api: true do
634 640 end
635 641 end
636 642  
637   - describe "GET /projects/:id/labels" do
638   - before { issue_with_labels }
  643 + describe 'GET /projects/:id/labels' do
  644 + context 'with an issue' do
  645 + before { issue_with_labels }
639 646  
640   - it "should return project labels" do
641   - get api("/projects/#{project.id}/labels", user)
642   - response.status.should == 200
643   - json_response.should be_an Array
644   - json_response.first['name'].should == issue_with_labels.labels.first.name
645   - json_response.last['name'].should == issue_with_labels.labels.last.name
  647 + it 'should return project labels' do
  648 + get api("/projects/#{project.id}/labels", user)
  649 + response.status.should == 200
  650 + json_response.should be_an Array
  651 + json_response.first['name'].should == issue_with_labels.labels.first.name
  652 + json_response.last['name'].should == issue_with_labels.labels.last.name
  653 + end
  654 + end
  655 +
  656 + context 'with a merge request' do
  657 + before { merge_request_with_labels }
  658 +
  659 + it 'should return project labels' do
  660 + get api("/projects/#{project.id}/labels", user)
  661 + response.status.should == 200
  662 + json_response.should be_an Array
  663 + json_response.first['name'].should == merge_request_with_labels.labels.first.name
  664 + json_response.last['name'].should == merge_request_with_labels.labels.last.name
  665 + end
  666 + end
  667 +
  668 + context 'with an issue and a merge request' do
  669 + before do
  670 + issue_with_labels
  671 + merge_request_with_labels
  672 + end
  673 +
  674 + it 'should return project labels from both' do
  675 + get api("/projects/#{project.id}/labels", user)
  676 + response.status.should == 200
  677 + json_response.should be_an Array
  678 + all_labels = issue_with_labels.labels.map(&:name).to_a
  679 + .concat(merge_request_with_labels.labels.map(&:name).to_a)
  680 + json_response.map { |e| e['name'] }.should =~ all_labels
  681 + end
646 682 end
647 683 end
648 684 end
... ...