##// END OF EJS Templates
Ability to define commit keywords per tracker (#7590)....
Jean-Philippe Lang -
r11978:b6cb7aa8e3b9
parent child
Show More
@@ -47,7 +47,7 class SettingsController < ApplicationController
47 @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
47 @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
48
48
49 @commit_update_keywords = Setting.commit_update_keywords.dup
49 @commit_update_keywords = Setting.commit_update_keywords.dup
50 @commit_update_keywords[''] = {} if @commit_update_keywords.blank?
50 @commit_update_keywords << {} if @commit_update_keywords.blank?
51
51
52 Redmine::Themes.rescan
52 Redmine::Themes.rescan
53 end
53 end
@@ -118,7 +118,7 class Changeset < ActiveRecord::Base
118 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
118 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
119 ref_keywords_any = ref_keywords.delete('*')
119 ref_keywords_any = ref_keywords.delete('*')
120 # keywords used to fix issues
120 # keywords used to fix issues
121 fix_keywords = Setting.commit_update_by_keyword.keys
121 fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
122
122
123 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
123 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
124
124
@@ -215,16 +215,18 class Changeset < ActiveRecord::Base
215
215
216 # Updates the +issue+ according to +action+
216 # Updates the +issue+ according to +action+
217 def fix_issue(issue, action)
217 def fix_issue(issue, action)
218 updates = Setting.commit_update_by_keyword[action]
219 return unless updates.is_a?(Hash)
220
221 # the issue may have been updated by the closure of another one (eg. duplicate)
218 # the issue may have been updated by the closure of another one (eg. duplicate)
222 issue.reload
219 issue.reload
223 # don't change the status is the issue is closed
220 # don't change the status is the issue is closed
224 return if issue.status && issue.status.is_closed?
221 return if issue.status && issue.status.is_closed?
225
222
226 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
223 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
227 issue.assign_attributes updates.slice(*Issue.attribute_names)
224 rule = Setting.commit_update_keywords_array.detect do |rule|
225 rule['keywords'].include?(action) && (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
226 end
227 if rule
228 issue.assign_attributes rule.slice(*Issue.attribute_names)
229 end
228 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
230 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
229 { :changeset => self, :issue => issue, :action => action })
231 { :changeset => self, :issue => issue, :action => action })
230 unless issue.save
232 unless issue.save
@@ -154,18 +154,18 END_SRC
154 # Example:
154 # Example:
155 # params = {:keywords => ['fixes', 'closes'], :status_id => ["3", "5"], :done_ratio => ["", "100"]}
155 # params = {:keywords => ['fixes', 'closes'], :status_id => ["3", "5"], :done_ratio => ["", "100"]}
156 # Setting.commit_update_keywords_from_params(params)
156 # Setting.commit_update_keywords_from_params(params)
157 # # => {'fixes' => {'status_id' => "3"}, 'closes' => {'status_id' => "5", 'done_ratio' => "100"}}
157 # # => [{'keywords => 'fixes', 'status_id' => "3"}, {'keywords => 'closes', 'status_id' => "5", 'done_ratio' => "100"}]
158 def self.commit_update_keywords_from_params(params)
158 def self.commit_update_keywords_from_params(params)
159 s = {}
159 s = []
160 if params.is_a?(Hash) && params.key?(:keywords) && params.values.all? {|v| v.is_a? Array}
160 if params.is_a?(Hash) && params.key?(:keywords) && params.values.all? {|v| v.is_a? Array}
161 attributes = params.except(:keywords).keys
161 attributes = params.except(:keywords).keys
162 params[:keywords].each_with_index do |keywords, i|
162 params[:keywords].each_with_index do |keywords, i|
163 next if keywords.blank?
163 next if keywords.blank?
164 s[keywords] = attributes.inject({}) {|h, a|
164 s << attributes.inject({}) {|h, a|
165 value = params[a][i].to_s
165 value = params[a][i].to_s
166 h[a.to_s] = value if value.present?
166 h[a.to_s] = value if value.present?
167 h
167 h
168 }
168 }.merge('keywords' => keywords)
169 end
169 end
170 end
170 end
171 s
171 s
@@ -177,39 +177,39 END_SRC
177 end
177 end
178
178
179 # Helper that returns a Hash with single update keywords as keys
179 # Helper that returns a Hash with single update keywords as keys
180 def self.commit_update_by_keyword
180 def self.commit_update_keywords_array
181 h = {}
181 a = []
182 if commit_update_keywords.is_a?(Hash)
182 if commit_update_keywords.is_a?(Array)
183 commit_update_keywords.each do |keywords, attribute_updates|
183 commit_update_keywords.each do |rule|
184 next unless attribute_updates.is_a?(Hash)
184 next unless rule.is_a?(Hash)
185 attribute_updates = attribute_updates.dup
185 rule = rule.dup
186 attribute_updates.delete_if {|k, v| v.blank?}
186 rule.delete_if {|k, v| v.blank?}
187 keywords.to_s.split(",").map(&:strip).reject(&:blank?).each do |keyword|
187 keywords = rule['keywords'].to_s.downcase.split(",").map(&:strip).reject(&:blank?)
188 h[keyword.downcase] = attribute_updates
188 next if keywords.empty?
189 end
189 a << rule.merge('keywords' => keywords)
190 end
190 end
191 end
191 end
192 h
192 a
193 end
193 end
194
194
195 def self.commit_fix_keywords
195 def self.commit_fix_keywords
196 ActiveSupport::Deprecation.warn "Setting.commit_fix_keywords is deprecated and will be removed in Redmine 3"
196 ActiveSupport::Deprecation.warn "Setting.commit_fix_keywords is deprecated and will be removed in Redmine 3"
197 if commit_update_keywords.is_a?(Hash)
197 if commit_update_keywords.is_a?(Array)
198 commit_update_keywords.keys.first
198 commit_update_keywords.first && commit_update_keywords.first['keywords']
199 end
199 end
200 end
200 end
201
201
202 def self.commit_fix_status_id
202 def self.commit_fix_status_id
203 ActiveSupport::Deprecation.warn "Setting.commit_fix_status_id is deprecated and will be removed in Redmine 3"
203 ActiveSupport::Deprecation.warn "Setting.commit_fix_status_id is deprecated and will be removed in Redmine 3"
204 if commit_update_keywords.is_a?(Hash)
204 if commit_update_keywords.is_a?(Array)
205 commit_update_keywords[commit_fix_keywords]['status_id']
205 commit_update_keywords.first && commit_update_keywords.first['status_id']
206 end
206 end
207 end
207 end
208
208
209 def self.commit_fix_done_ratio
209 def self.commit_fix_done_ratio
210 ActiveSupport::Deprecation.warn "Setting.commit_fix_done_ratio is deprecated and will be removed in Redmine 3"
210 ActiveSupport::Deprecation.warn "Setting.commit_fix_done_ratio is deprecated and will be removed in Redmine 3"
211 if commit_update_keywords.is_a?(Hash)
211 if commit_update_keywords.is_a?(Array)
212 commit_update_keywords[commit_fix_keywords]['done_ratio']
212 commit_update_keywords.first && commit_update_keywords.first['done_ratio']
213 end
213 end
214 end
214 end
215
215
@@ -80,6 +80,7
80 <table class="list" id="commit-keywords">
80 <table class="list" id="commit-keywords">
81 <thead>
81 <thead>
82 <tr>
82 <tr>
83 <th><%= l(:label_tracker) %></th>
83 <th><%= l(:setting_commit_fix_keywords) %></th>
84 <th><%= l(:setting_commit_fix_keywords) %></th>
84 <th><%= l(:label_applied_status) %></th>
85 <th><%= l(:label_applied_status) %></th>
85 <th><%= l(:field_done_ratio) %></th>
86 <th><%= l(:field_done_ratio) %></th>
@@ -87,15 +88,17
87 </tr>
88 </tr>
88 </thead>
89 </thead>
89 <tbody>
90 <tbody>
90 <% @commit_update_keywords.each do |keywords, updates| %>
91 <% @commit_update_keywords.each do |rule| %>
91 <tr class="commit-keywords">
92 <tr class="commit-keywords">
92 <td><%= text_field_tag "settings[commit_update_keywords][keywords][]", keywords, :size => 30 %></td>
93 <td><%= select_tag "settings[commit_update_keywords][if_tracker_id][]", options_for_select([[l(:label_all), ""]] + Tracker.sorted.all.map {|t| [t.name, t.id.to_s]}, rule['if_tracker_id']) %></td>
93 <td><%= select_tag "settings[commit_update_keywords][status_id][]", options_for_select([["", 0]] + IssueStatus.sorted.all.collect{|status| [status.name, status.id.to_s]}, updates['status_id']) %></td>
94 <td><%= text_field_tag "settings[commit_update_keywords][keywords][]", rule['keywords'], :size => 30 %></td>
94 <td><%= select_tag "settings[commit_update_keywords][done_ratio][]", options_for_select([["", ""]] + (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] }, updates['done_ratio']) %></td>
95 <td><%= select_tag "settings[commit_update_keywords][status_id][]", options_for_select([["", 0]] + IssueStatus.sorted.all.collect{|status| [status.name, status.id.to_s]}, rule['status_id']) %></td>
96 <td><%= select_tag "settings[commit_update_keywords][done_ratio][]", options_for_select([["", ""]] + (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] }, rule['done_ratio']) %></td>
95 <td class="buttons"><%= link_to image_tag('delete.png'), '#', :class => 'delete-commit-keywords' %></td>
97 <td class="buttons"><%= link_to image_tag('delete.png'), '#', :class => 'delete-commit-keywords' %></td>
96 </tr>
98 </tr>
97 <% end %>
99 <% end %>
98 <tr>
100 <tr>
101 <td></td>
99 <td><em class="info"><%= l(:text_comma_separated) %></em></td>
102 <td><em class="info"><%= l(:text_comma_separated) %></em></td>
100 <td></td>
103 <td></td>
101 <td></td>
104 <td></td>
@@ -108,7 +108,7 commit_ref_keywords:
108 default: 'refs,references,IssueID'
108 default: 'refs,references,IssueID'
109 commit_update_keywords:
109 commit_update_keywords:
110 serialized: true
110 serialized: true
111 default: {}
111 default: []
112 commit_logtime_enabled:
112 commit_logtime_enabled:
113 default: 0
113 default: 0
114 commit_logtime_activity_id:
114 commit_logtime_activity_id:
@@ -81,38 +81,41 class SettingsControllerTest < ActionController::TestCase
81 end
81 end
82
82
83 def test_edit_commit_update_keywords
83 def test_edit_commit_update_keywords
84 with_settings :commit_update_keywords => {
84 with_settings :commit_update_keywords => [
85 "fixes, resolves" => {"status_id" => "3"},
85 {"keywords" => "fixes, resolves", "status_id" => "3"},
86 "closes" => {"status_id" => "5", "done_ratio" => "100"}
86 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
87 } do
87 ] do
88 get :edit
88 get :edit
89 end
89 end
90 assert_response :success
90 assert_response :success
91 assert_select 'tr.commit-keywords', 2
91 assert_select 'tr.commit-keywords', 2
92 assert_select 'tr.commit-keywords' do
92 assert_select 'tr.commit-keywords:nth-child(1)' do
93 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
93 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
94 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
94 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
95 assert_select 'option[value=3][selected=selected]'
95 assert_select 'option[value=3][selected=selected]'
96 end
96 end
97 end
97 end
98 assert_select 'tr.commit-keywords' do
98 assert_select 'tr.commit-keywords:nth-child(2)' do
99 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
99 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
100 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
100 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
101 assert_select 'option[value=5][selected=selected]'
101 assert_select 'option[value=5][selected=selected]', :text => 'Closed'
102 end
102 end
103 assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
103 assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
104 assert_select 'option[value=100][selected=selected]'
104 assert_select 'option[value=100][selected=selected]', :text => '100 %'
105 end
106 assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do
107 assert_select 'option[value=2][selected=selected]', :text => 'Feature request'
105 end
108 end
106 end
109 end
107 end
110 end
108
111
109 def test_edit_without_commit_update_keywords_should_show_blank_line
112 def test_edit_without_commit_update_keywords_should_show_blank_line
110 with_settings :commit_update_keywords => {} do
113 with_settings :commit_update_keywords => [] do
111 get :edit
114 get :edit
112 end
115 end
113 assert_response :success
116 assert_response :success
114 assert_select 'tr.commit-keywords', 1 do
117 assert_select 'tr.commit-keywords', 1 do
115 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', ''
118 assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]'
116 end
119 end
117 end
120 end
118
121
@@ -121,14 +124,15 class SettingsControllerTest < ActionController::TestCase
121 :commit_update_keywords => {
124 :commit_update_keywords => {
122 :keywords => ["resolves", "closes"],
125 :keywords => ["resolves", "closes"],
123 :status_id => ["3", "5"],
126 :status_id => ["3", "5"],
124 :done_ratio => ["", "100"]
127 :done_ratio => ["", "100"],
128 :if_tracker_id => ["", "2"]
125 }
129 }
126 }
130 }
127 assert_redirected_to '/settings'
131 assert_redirected_to '/settings'
128 assert_equal({
132 assert_equal([
129 "resolves" => {"status_id" => "3"},
133 {"keywords" => "resolves", "status_id" => "3"},
130 "closes" => {"status_id" => "5", "done_ratio" => "100"}
134 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
131 }, Setting.commit_update_keywords)
135 ], Setting.commit_update_keywords)
132 end
136 end
133
137
134 def test_get_plugin_settings
138 def test_get_plugin_settings
@@ -166,4 +166,16 module ObjectHelpers
166 field.save!
166 field.save!
167 field
167 field
168 end
168 end
169
170 def Changeset.generate!(attributes={})
171 @generated_changeset_rev ||= '123456'
172 @generated_changeset_rev.succ!
173 changeset = new(attributes)
174 changeset.repository ||= Project.find(1).repository
175 changeset.revision ||= @generated_changeset_rev
176 changeset.committed_on ||= Time.now
177 yield changeset if block_given?
178 changeset.save!
179 changeset
180 end
169 end
181 end
@@ -31,7 +31,7 class ChangesetTest < ActiveSupport::TestCase
31 def test_ref_keywords_any
31 def test_ref_keywords_any
32 ActionMailer::Base.deliveries.clear
32 ActionMailer::Base.deliveries.clear
33 Setting.commit_ref_keywords = '*'
33 Setting.commit_ref_keywords = '*'
34 Setting.commit_update_keywords = {'fixes , closes' => {'status_id' => '5', 'done_ratio' => '90'}}
34 Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', 'status_id' => '5', 'done_ratio' => '90'}]
35
35
36 c = Changeset.new(:repository => Project.find(1).repository,
36 c = Changeset.new(:repository => Project.find(1).repository,
37 :committed_on => Time.now,
37 :committed_on => Time.now,
@@ -111,11 +111,7 class ChangesetTest < ActiveSupport::TestCase
111
111
112 def test_ref_keywords_closing_with_timelog
112 def test_ref_keywords_closing_with_timelog
113 Setting.commit_ref_keywords = '*'
113 Setting.commit_ref_keywords = '*'
114 Setting.commit_update_keywords = {
114 Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', 'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s}]
115 'fixes , closes' => {
116 'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s
117 }
118 }
119 Setting.commit_logtime_enabled = '1'
115 Setting.commit_logtime_enabled = '1'
120
116
121 c = Changeset.new(:repository => Project.find(1).repository,
117 c = Changeset.new(:repository => Project.find(1).repository,
@@ -165,20 +161,45 class ChangesetTest < ActiveSupport::TestCase
165 end
161 end
166
162
167 def test_update_keywords_with_multiple_rules
163 def test_update_keywords_with_multiple_rules
168 Setting.commit_update_keywords = {
164 with_settings :commit_update_keywords => [
169 'fixes, closes' => {'status_id' => '5'},
165 {'keywords' => 'fixes, closes', 'status_id' => '5'},
170 'resolves' => {'status_id' => '3'}
166 {'keywords' => 'resolves', 'status_id' => '3'}
171 }
167 ] do
172 issue1 = Issue.generate!
168
173 issue2 = Issue.generate!
169 issue1 = Issue.generate!
170 issue2 = Issue.generate!
171 Changeset.generate!(:comments => "Closes ##{issue1.id}\nResolves ##{issue2.id}")
172 assert_equal 5, issue1.reload.status_id
173 assert_equal 3, issue2.reload.status_id
174 end
175 end
174
176
175 c = Changeset.new(:repository => Project.find(1).repository,
177 def test_update_keywords_with_multiple_rules_should_match_tracker
176 :committed_on => Time.now,
178 with_settings :commit_update_keywords => [
177 :comments => "Closes ##{issue1.id}\nResolves ##{issue2.id}",
179 {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
178 :revision => '12345')
180 {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => ''}
179 assert c.save
181 ] do
180 assert_equal 5, issue1.reload.status_id
182
181 assert_equal 3, issue2.reload.status_id
183 issue1 = Issue.generate!(:tracker_id => 2)
184 issue2 = Issue.generate!
185 Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
186 assert_equal 5, issue1.reload.status_id
187 assert_equal 3, issue2.reload.status_id
188 end
189 end
190
191 def test_update_keywords_with_multiple_rules_and_no_match
192 with_settings :commit_update_keywords => [
193 {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
194 {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => '3'}
195 ] do
196
197 issue1 = Issue.generate!(:tracker_id => 2)
198 issue2 = Issue.generate!
199 Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
200 assert_equal 5, issue1.reload.status_id
201 assert_equal 1, issue2.reload.status_id # no updates
202 end
182 end
203 end
183
204
184 def test_commit_referencing_a_subproject_issue
205 def test_commit_referencing_a_subproject_issue
@@ -192,7 +213,7 class ChangesetTest < ActiveSupport::TestCase
192 end
213 end
193
214
194 def test_commit_closing_a_subproject_issue
215 def test_commit_closing_a_subproject_issue
195 with_settings :commit_update_keywords => {'closes' => {'status_id' => '5'}},
216 with_settings :commit_update_keywords => [{'keywords' => 'closes', 'status_id' => '5'}],
196 :default_language => 'en' do
217 :default_language => 'en' do
197 issue = Issue.find(5)
218 issue = Issue.find(5)
198 assert !issue.closed?
219 assert !issue.closed?
@@ -183,9 +183,9 class RepositoryTest < ActiveSupport::TestCase
183 Setting.default_language = 'en'
183 Setting.default_language = 'en'
184
184
185 Setting.commit_ref_keywords = 'refs , references, IssueID'
185 Setting.commit_ref_keywords = 'refs , references, IssueID'
186 Setting.commit_update_keywords = {
186 Setting.commit_update_keywords = [
187 'fixes , closes' => {'status_id' => IssueStatus.where(:is_closed => true).first.id, 'done_ratio' => '90'}
187 {'keywords' => 'fixes , closes', 'status_id' => IssueStatus.where(:is_closed => true).first.id, 'done_ratio' => '90'}
188 }
188 ]
189 Setting.default_language = 'en'
189 Setting.default_language = 'en'
190 ActionMailer::Base.deliveries.clear
190 ActionMailer::Base.deliveries.clear
191
191
General Comments 0
You need to be logged in to leave comments. Login now