Ability to define commit keywords per tracker (#7590).

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@12208 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang 2013-10-13 07:37:49 +00:00
parent b8aa4da28a
commit b6cb7aa8e3
9 changed files with 115 additions and 73 deletions

View File

@ -47,7 +47,7 @@ class SettingsController < ApplicationController
@guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank? @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
@commit_update_keywords = Setting.commit_update_keywords.dup @commit_update_keywords = Setting.commit_update_keywords.dup
@commit_update_keywords[''] = {} if @commit_update_keywords.blank? @commit_update_keywords << {} if @commit_update_keywords.blank?
Redmine::Themes.rescan Redmine::Themes.rescan
end end

View File

@ -118,7 +118,7 @@ class Changeset < ActiveRecord::Base
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip) ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
ref_keywords_any = ref_keywords.delete('*') ref_keywords_any = ref_keywords.delete('*')
# keywords used to fix issues # keywords used to fix issues
fix_keywords = Setting.commit_update_by_keyword.keys fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|") kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
@ -215,16 +215,18 @@ class Changeset < ActiveRecord::Base
# Updates the +issue+ according to +action+ # Updates the +issue+ according to +action+
def fix_issue(issue, action) def fix_issue(issue, action)
updates = Setting.commit_update_by_keyword[action]
return unless updates.is_a?(Hash)
# the issue may have been updated by the closure of another one (eg. duplicate) # the issue may have been updated by the closure of another one (eg. duplicate)
issue.reload issue.reload
# don't change the status is the issue is closed # don't change the status is the issue is closed
return if issue.status && issue.status.is_closed? return if issue.status && issue.status.is_closed?
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project))) journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
issue.assign_attributes updates.slice(*Issue.attribute_names) rule = Setting.commit_update_keywords_array.detect do |rule|
rule['keywords'].include?(action) && (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
end
if rule
issue.assign_attributes rule.slice(*Issue.attribute_names)
end
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update, Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
{ :changeset => self, :issue => issue, :action => action }) { :changeset => self, :issue => issue, :action => action })
unless issue.save unless issue.save

View File

@ -154,18 +154,18 @@ END_SRC
# Example: # Example:
# params = {:keywords => ['fixes', 'closes'], :status_id => ["3", "5"], :done_ratio => ["", "100"]} # params = {:keywords => ['fixes', 'closes'], :status_id => ["3", "5"], :done_ratio => ["", "100"]}
# Setting.commit_update_keywords_from_params(params) # Setting.commit_update_keywords_from_params(params)
# # => {'fixes' => {'status_id' => "3"}, 'closes' => {'status_id' => "5", 'done_ratio' => "100"}} # # => [{'keywords => 'fixes', 'status_id' => "3"}, {'keywords => 'closes', 'status_id' => "5", 'done_ratio' => "100"}]
def self.commit_update_keywords_from_params(params) def self.commit_update_keywords_from_params(params)
s = {} s = []
if params.is_a?(Hash) && params.key?(:keywords) && params.values.all? {|v| v.is_a? Array} if params.is_a?(Hash) && params.key?(:keywords) && params.values.all? {|v| v.is_a? Array}
attributes = params.except(:keywords).keys attributes = params.except(:keywords).keys
params[:keywords].each_with_index do |keywords, i| params[:keywords].each_with_index do |keywords, i|
next if keywords.blank? next if keywords.blank?
s[keywords] = attributes.inject({}) {|h, a| s << attributes.inject({}) {|h, a|
value = params[a][i].to_s value = params[a][i].to_s
h[a.to_s] = value if value.present? h[a.to_s] = value if value.present?
h h
} }.merge('keywords' => keywords)
end end
end end
s s
@ -177,39 +177,39 @@ END_SRC
end end
# Helper that returns a Hash with single update keywords as keys # Helper that returns a Hash with single update keywords as keys
def self.commit_update_by_keyword def self.commit_update_keywords_array
h = {} a = []
if commit_update_keywords.is_a?(Hash) if commit_update_keywords.is_a?(Array)
commit_update_keywords.each do |keywords, attribute_updates| commit_update_keywords.each do |rule|
next unless attribute_updates.is_a?(Hash) next unless rule.is_a?(Hash)
attribute_updates = attribute_updates.dup rule = rule.dup
attribute_updates.delete_if {|k, v| v.blank?} rule.delete_if {|k, v| v.blank?}
keywords.to_s.split(",").map(&:strip).reject(&:blank?).each do |keyword| keywords = rule['keywords'].to_s.downcase.split(",").map(&:strip).reject(&:blank?)
h[keyword.downcase] = attribute_updates next if keywords.empty?
a << rule.merge('keywords' => keywords)
end end
end end
end a
h
end end
def self.commit_fix_keywords def self.commit_fix_keywords
ActiveSupport::Deprecation.warn "Setting.commit_fix_keywords is deprecated and will be removed in Redmine 3" ActiveSupport::Deprecation.warn "Setting.commit_fix_keywords is deprecated and will be removed in Redmine 3"
if commit_update_keywords.is_a?(Hash) if commit_update_keywords.is_a?(Array)
commit_update_keywords.keys.first commit_update_keywords.first && commit_update_keywords.first['keywords']
end end
end end
def self.commit_fix_status_id def self.commit_fix_status_id
ActiveSupport::Deprecation.warn "Setting.commit_fix_status_id is deprecated and will be removed in Redmine 3" ActiveSupport::Deprecation.warn "Setting.commit_fix_status_id is deprecated and will be removed in Redmine 3"
if commit_update_keywords.is_a?(Hash) if commit_update_keywords.is_a?(Array)
commit_update_keywords[commit_fix_keywords]['status_id'] commit_update_keywords.first && commit_update_keywords.first['status_id']
end end
end end
def self.commit_fix_done_ratio def self.commit_fix_done_ratio
ActiveSupport::Deprecation.warn "Setting.commit_fix_done_ratio is deprecated and will be removed in Redmine 3" ActiveSupport::Deprecation.warn "Setting.commit_fix_done_ratio is deprecated and will be removed in Redmine 3"
if commit_update_keywords.is_a?(Hash) if commit_update_keywords.is_a?(Array)
commit_update_keywords[commit_fix_keywords]['done_ratio'] commit_update_keywords.first && commit_update_keywords.first['done_ratio']
end end
end end

View File

@ -80,6 +80,7 @@
<table class="list" id="commit-keywords"> <table class="list" id="commit-keywords">
<thead> <thead>
<tr> <tr>
<th><%= l(:label_tracker) %></th>
<th><%= l(:setting_commit_fix_keywords) %></th> <th><%= l(:setting_commit_fix_keywords) %></th>
<th><%= l(:label_applied_status) %></th> <th><%= l(:label_applied_status) %></th>
<th><%= l(:field_done_ratio) %></th> <th><%= l(:field_done_ratio) %></th>
@ -87,15 +88,17 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% @commit_update_keywords.each do |keywords, updates| %> <% @commit_update_keywords.each do |rule| %>
<tr class="commit-keywords"> <tr class="commit-keywords">
<td><%= text_field_tag "settings[commit_update_keywords][keywords][]", keywords, :size => 30 %></td> <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>
<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> <td><%= text_field_tag "settings[commit_update_keywords][keywords][]", rule['keywords'], :size => 30 %></td>
<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> <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>
<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>
<td class="buttons"><%= link_to image_tag('delete.png'), '#', :class => 'delete-commit-keywords' %></td> <td class="buttons"><%= link_to image_tag('delete.png'), '#', :class => 'delete-commit-keywords' %></td>
</tr> </tr>
<% end %> <% end %>
<tr> <tr>
<td></td>
<td><em class="info"><%= l(:text_comma_separated) %></em></td> <td><em class="info"><%= l(:text_comma_separated) %></em></td>
<td></td> <td></td>
<td></td> <td></td>

View File

@ -108,7 +108,7 @@ commit_ref_keywords:
default: 'refs,references,IssueID' default: 'refs,references,IssueID'
commit_update_keywords: commit_update_keywords:
serialized: true serialized: true
default: {} default: []
commit_logtime_enabled: commit_logtime_enabled:
default: 0 default: 0
commit_logtime_activity_id: commit_logtime_activity_id:

View File

@ -81,38 +81,41 @@ class SettingsControllerTest < ActionController::TestCase
end end
def test_edit_commit_update_keywords def test_edit_commit_update_keywords
with_settings :commit_update_keywords => { with_settings :commit_update_keywords => [
"fixes, resolves" => {"status_id" => "3"}, {"keywords" => "fixes, resolves", "status_id" => "3"},
"closes" => {"status_id" => "5", "done_ratio" => "100"} {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
} do ] do
get :edit get :edit
end end
assert_response :success assert_response :success
assert_select 'tr.commit-keywords', 2 assert_select 'tr.commit-keywords', 2
assert_select 'tr.commit-keywords' do assert_select 'tr.commit-keywords:nth-child(1)' do
assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves' assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
assert_select 'option[value=3][selected=selected]' assert_select 'option[value=3][selected=selected]'
end end
end end
assert_select 'tr.commit-keywords' do assert_select 'tr.commit-keywords:nth-child(2)' do
assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes' assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
assert_select 'option[value=5][selected=selected]' assert_select 'option[value=5][selected=selected]', :text => 'Closed'
end end
assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
assert_select 'option[value=100][selected=selected]' assert_select 'option[value=100][selected=selected]', :text => '100 %'
end
assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do
assert_select 'option[value=2][selected=selected]', :text => 'Feature request'
end end
end end
end end
def test_edit_without_commit_update_keywords_should_show_blank_line def test_edit_without_commit_update_keywords_should_show_blank_line
with_settings :commit_update_keywords => {} do with_settings :commit_update_keywords => [] do
get :edit get :edit
end end
assert_response :success assert_response :success
assert_select 'tr.commit-keywords', 1 do assert_select 'tr.commit-keywords', 1 do
assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', '' assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]'
end end
end end
@ -121,14 +124,15 @@ class SettingsControllerTest < ActionController::TestCase
:commit_update_keywords => { :commit_update_keywords => {
:keywords => ["resolves", "closes"], :keywords => ["resolves", "closes"],
:status_id => ["3", "5"], :status_id => ["3", "5"],
:done_ratio => ["", "100"] :done_ratio => ["", "100"],
:if_tracker_id => ["", "2"]
} }
} }
assert_redirected_to '/settings' assert_redirected_to '/settings'
assert_equal({ assert_equal([
"resolves" => {"status_id" => "3"}, {"keywords" => "resolves", "status_id" => "3"},
"closes" => {"status_id" => "5", "done_ratio" => "100"} {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
}, Setting.commit_update_keywords) ], Setting.commit_update_keywords)
end end
def test_get_plugin_settings def test_get_plugin_settings

View File

@ -166,4 +166,16 @@ module ObjectHelpers
field.save! field.save!
field field
end end
def Changeset.generate!(attributes={})
@generated_changeset_rev ||= '123456'
@generated_changeset_rev.succ!
changeset = new(attributes)
changeset.repository ||= Project.find(1).repository
changeset.revision ||= @generated_changeset_rev
changeset.committed_on ||= Time.now
yield changeset if block_given?
changeset.save!
changeset
end
end end

View File

@ -31,7 +31,7 @@ class ChangesetTest < ActiveSupport::TestCase
def test_ref_keywords_any def test_ref_keywords_any
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
Setting.commit_ref_keywords = '*' Setting.commit_ref_keywords = '*'
Setting.commit_update_keywords = {'fixes , closes' => {'status_id' => '5', 'done_ratio' => '90'}} Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', 'status_id' => '5', 'done_ratio' => '90'}]
c = Changeset.new(:repository => Project.find(1).repository, c = Changeset.new(:repository => Project.find(1).repository,
:committed_on => Time.now, :committed_on => Time.now,
@ -111,11 +111,7 @@ class ChangesetTest < ActiveSupport::TestCase
def test_ref_keywords_closing_with_timelog def test_ref_keywords_closing_with_timelog
Setting.commit_ref_keywords = '*' Setting.commit_ref_keywords = '*'
Setting.commit_update_keywords = { Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', 'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s}]
'fixes , closes' => {
'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s
}
}
Setting.commit_logtime_enabled = '1' Setting.commit_logtime_enabled = '1'
c = Changeset.new(:repository => Project.find(1).repository, c = Changeset.new(:repository => Project.find(1).repository,
@ -165,21 +161,46 @@ class ChangesetTest < ActiveSupport::TestCase
end end
def test_update_keywords_with_multiple_rules def test_update_keywords_with_multiple_rules
Setting.commit_update_keywords = { with_settings :commit_update_keywords => [
'fixes, closes' => {'status_id' => '5'}, {'keywords' => 'fixes, closes', 'status_id' => '5'},
'resolves' => {'status_id' => '3'} {'keywords' => 'resolves', 'status_id' => '3'}
} ] do
issue1 = Issue.generate! issue1 = Issue.generate!
issue2 = Issue.generate! issue2 = Issue.generate!
Changeset.generate!(:comments => "Closes ##{issue1.id}\nResolves ##{issue2.id}")
c = Changeset.new(:repository => Project.find(1).repository,
:committed_on => Time.now,
:comments => "Closes ##{issue1.id}\nResolves ##{issue2.id}",
:revision => '12345')
assert c.save
assert_equal 5, issue1.reload.status_id assert_equal 5, issue1.reload.status_id
assert_equal 3, issue2.reload.status_id assert_equal 3, issue2.reload.status_id
end end
end
def test_update_keywords_with_multiple_rules_should_match_tracker
with_settings :commit_update_keywords => [
{'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
{'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => ''}
] do
issue1 = Issue.generate!(:tracker_id => 2)
issue2 = Issue.generate!
Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
assert_equal 5, issue1.reload.status_id
assert_equal 3, issue2.reload.status_id
end
end
def test_update_keywords_with_multiple_rules_and_no_match
with_settings :commit_update_keywords => [
{'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
{'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => '3'}
] do
issue1 = Issue.generate!(:tracker_id => 2)
issue2 = Issue.generate!
Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
assert_equal 5, issue1.reload.status_id
assert_equal 1, issue2.reload.status_id # no updates
end
end
def test_commit_referencing_a_subproject_issue def test_commit_referencing_a_subproject_issue
c = Changeset.new(:repository => Project.find(1).repository, c = Changeset.new(:repository => Project.find(1).repository,
@ -192,7 +213,7 @@ class ChangesetTest < ActiveSupport::TestCase
end end
def test_commit_closing_a_subproject_issue def test_commit_closing_a_subproject_issue
with_settings :commit_update_keywords => {'closes' => {'status_id' => '5'}}, with_settings :commit_update_keywords => [{'keywords' => 'closes', 'status_id' => '5'}],
:default_language => 'en' do :default_language => 'en' do
issue = Issue.find(5) issue = Issue.find(5)
assert !issue.closed? assert !issue.closed?

View File

@ -183,9 +183,9 @@ class RepositoryTest < ActiveSupport::TestCase
Setting.default_language = 'en' Setting.default_language = 'en'
Setting.commit_ref_keywords = 'refs , references, IssueID' Setting.commit_ref_keywords = 'refs , references, IssueID'
Setting.commit_update_keywords = { Setting.commit_update_keywords = [
'fixes , closes' => {'status_id' => IssueStatus.where(:is_closed => true).first.id, 'done_ratio' => '90'} {'keywords' => 'fixes , closes', 'status_id' => IssueStatus.where(:is_closed => true).first.id, 'done_ratio' => '90'}
} ]
Setting.default_language = 'en' Setting.default_language = 'en'
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear