Commit 1f1c59b61d30b76d69f0f925b43a0b96465f38ed
Exists in
spb-stable
and in
2 other branches
Merge pull request #6778 from dblessing/feature/mr_labels
Merge Request Labels
Showing
15 changed files
with
142 additions
and
17 deletions
Show diff stats
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 < 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
... | ... | @@ -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 | + | ... | ... |
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 | ... | ... |