Commit 5f25cdfe19c7c0a8c1ada592307e9017e2a754e1
1 parent
172ad962
Exists in
spb-stable
and in
2 other branches
Implement Merge Request Labels
Showing
15 changed files
with
142 additions
and
17 deletions
Show diff stats
app/assets/stylesheets/sections/merge_requests.scss
... | ... | @@ -74,6 +74,10 @@ |
74 | 74 | |
75 | 75 | .merge-request-info { |
76 | 76 | color: #999; |
77 | + | |
78 | + .merge-request-labels { | |
79 | + display: inline-block; | |
80 | + } | |
77 | 81 | } |
78 | 82 | } |
79 | 83 | } |
... | ... | @@ -112,3 +116,7 @@ |
112 | 116 | } |
113 | 117 | } |
114 | 118 | } |
119 | + | |
120 | +.merge-request-show-labels .label { | |
121 | + padding: 6px 10px; | |
122 | +} | ... | ... |
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 < 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 < 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 < 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 < 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
... | ... | @@ -52,6 +52,15 @@ |
52 | 52 | = f.text_area :description, class: "form-control js-gfm-input", rows: 14 |
53 | 53 | %p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. |
54 | 54 | |
55 | + - if @merge_request.persisted? # Only allow labels on edit to avoid fork vs upstream repo labels issue | |
56 | + .form-group | |
57 | + = f.label :label_list, class: 'control-label' do | |
58 | + %i.icon-tag | |
59 | + Labels | |
60 | + .col-sm-10 | |
61 | + = f.text_field :label_list, maxlength: 2000, class: "form-control" | |
62 | + %p.hint Separate labels with commas. | |
63 | + | |
55 | 64 | .form-actions |
56 | 65 | - if @merge_request.new_record? |
57 | 66 | = f.submit 'Submit merge request', class: "btn btn-create" |
... | ... | @@ -83,3 +92,32 @@ |
83 | 92 | target_branch.on("change", function() { |
84 | 93 | $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); |
85 | 94 | }); |
95 | + | |
96 | + $("#merge_request_label_list") | |
97 | + .bind( "keydown", function( event ) { | |
98 | + if ( event.keyCode === $.ui.keyCode.TAB && | |
99 | + $( this ).data( "autocomplete" ).menu.active ) { | |
100 | + event.preventDefault(); | |
101 | + } | |
102 | + }) | |
103 | + .bind("click", function(event) { | |
104 | + $(this).autocomplete("search", ""); | |
105 | + }) | |
106 | + .autocomplete({ | |
107 | + minLength: 0, | |
108 | + source: function( request, response ) { | |
109 | + response( $.ui.autocomplete.filter( | |
110 | + #{raw labels_autocomplete_source}, extractLast( request.term ) ) ); | |
111 | + }, | |
112 | + focus: function() { | |
113 | + return false; | |
114 | + }, | |
115 | + select: function(event, ui) { | |
116 | + var terms = split( this.value ); | |
117 | + terms.pop(); | |
118 | + terms.push( ui.item.value ); | |
119 | + terms.push( "" ); | |
120 | + this.value = terms.join( ", " ); | |
121 | + return false; | |
122 | + } | |
123 | + }); | ... | ... |
app/views/projects/merge_requests/_merge_request.html.haml
... | ... | @@ -35,3 +35,9 @@ |
35 | 35 | |
36 | 36 | .pull-right |
37 | 37 | %small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')} |
38 | + | |
39 | + .merge-request-labels | |
40 | + - merge_request.labels.each do |label| | |
41 | + %span{class: "label #{label_css_class(label.name)}"} | |
42 | + %i.icon-tag | |
43 | + = 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 | + | ... | ... |
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
... | ... | @@ -135,6 +135,7 @@ module API |
135 | 135 | expose :target_branch, :source_branch, :upvotes, :downvotes |
136 | 136 | expose :author, :assignee, using: Entities::UserBasic |
137 | 137 | expose :source_project_id, :target_project_id |
138 | + expose :label_list, as: :labels | |
138 | 139 | end |
139 | 140 | |
140 | 141 | 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 | ... | ... |