]> source.dussan.org Git - redmine.git/commitdiff
Support for multiple issue update keywords/rules in commit messages (#4911).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 5 Oct 2013 09:41:11 +0000 (09:41 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 5 Oct 2013 09:41:11 +0000 (09:41 +0000)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@12197 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/controllers/settings_controller.rb
app/models/changeset.rb
app/models/setting.rb
app/views/settings/_repositories.html.erb
config/settings.yml
db/migrate/20131004113137_support_for_multiple_commit_keywords.rb [new file with mode: 0644]
test/functional/settings_controller_test.rb
test/unit/changeset_test.rb
test/unit/repository_test.rb

index 586c239568b06e656365bcd380e02b8de265ee6e..05550bc65f1301d3ad0a3942571f927c6c395ef6 100644 (file)
@@ -33,9 +33,7 @@ class SettingsController < ApplicationController
     if request.post? && params[:settings] && params[:settings].is_a?(Hash)
       settings = (params[:settings] || {}).dup.symbolize_keys
       settings.each do |name, value|
-        # remove blank values in array settings
-        value.delete_if {|v| v.blank? } if value.is_a?(Array)
-        Setting[name] = value
+        Setting.set_from_params name, value
       end
       flash[:notice] = l(:notice_successful_update)
       redirect_to settings_path(:tab => params[:tab])
@@ -48,6 +46,9 @@ class SettingsController < ApplicationController
       @guessed_host_and_path = request.host_with_port.dup
       @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[''] = {} if @commit_update_keywords.blank?
+
       Redmine::Themes.rescan
     end
   end
index d3fa8ab2be2269f19b774b21d9bae0886a4c36e9..7920ae078edf16fd761af86e66c9a6977af3978c 100644 (file)
@@ -118,21 +118,21 @@ class Changeset < ActiveRecord::Base
     ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
     ref_keywords_any = ref_keywords.delete('*')
     # keywords used to fix issues
-    fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
+    fix_keywords = Setting.commit_update_by_keyword.keys
 
     kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
 
     referenced_issues = []
 
     comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
-      action, refs = match[2], match[3]
+      action, refs = match[2].to_s.downcase, match[3]
       next unless action.present? || ref_keywords_any
 
       refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
         issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
         if issue
           referenced_issues << issue
-          fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
+          fix_issue(issue, action) if fix_keywords.include?(action)
           log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
         end
       end
@@ -210,12 +210,10 @@ class Changeset < ActiveRecord::Base
 
   private
 
-  def fix_issue(issue)
-    status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
-    if status.nil?
-      logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
-      return issue
-    end
+  # Updates the +issue+ according to +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)
     issue.reload
@@ -223,10 +221,7 @@ class Changeset < ActiveRecord::Base
     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)))
-    issue.status = status
-    unless Setting.commit_fix_done_ratio.blank?
-      issue.done_ratio = Setting.commit_fix_done_ratio.to_i
-    end
+    issue.assign_attributes updates.slice(*Issue.attribute_names)
     Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
                             { :changeset => self, :issue => issue })
     unless issue.save
index a0ab18b5b5b96cfe502f376cf0d3a497f689573b..71ef152083bd060c2a87e6d5b9377fbd3fd31545 100644 (file)
@@ -132,15 +132,87 @@ class Setting < ActiveRecord::Base
     def self.#{name}=(value)
       self[:#{name}] = value
     end
-    END_SRC
+END_SRC
     class_eval src, __FILE__, __LINE__
   end
 
+  # Sets a setting value from params
+  def self.set_from_params(name, params)
+    params = params.dup
+    params.delete_if {|v| v.blank? } if params.is_a?(Array)
+
+    m = "#{name}_from_params"
+    if respond_to? m
+      self[name.to_sym] = send m, params
+    else
+      self[name.to_sym] = params
+    end
+  end
+
+  # Returns a hash suitable for commit_update_keywords setting
+  #
+  # Example:
+  # params = {:keywords => ['fixes', 'closes'], :status_id => ["3", "5"], :done_ratio => ["", "100"]}
+  # Setting.commit_update_keywords_from_params(params)
+  # # => {'fixes' => {'status_id' => "3"}, 'closes' => {'status_id' => "5", 'done_ratio' => "100"}}
+  def self.commit_update_keywords_from_params(params)
+    s = {}
+    if params.is_a?(Hash) && params.key?(:keywords) && params.values.all? {|v| v.is_a? Array}
+      attributes = params.except(:keywords).keys
+      params[:keywords].each_with_index do |keywords, i|
+        next if keywords.blank?
+        s[keywords] = attributes.inject({}) {|h, a|
+          value = params[a][i].to_s
+          h[a.to_s] = value if value.present?
+          h
+        }
+      end
+    end
+    s
+  end
+
   # Helper that returns an array based on per_page_options setting
   def self.per_page_options_array
     per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
   end
 
+  # Helper that returns a Hash with single update keywords as keys
+  def self.commit_update_by_keyword
+    h = {}
+    if commit_update_keywords.is_a?(Hash)
+      commit_update_keywords.each do |keywords, attribute_updates|
+        next unless attribute_updates.is_a?(Hash)
+        attribute_updates = attribute_updates.dup
+        attribute_updates.delete_if {|k, v| v.blank?}
+        keywords.to_s.split(",").map(&:strip).reject(&:blank?).each do |keyword|
+          h[keyword.downcase] = attribute_updates
+        end
+      end
+    end
+    h
+  end
+
+  def self.commit_fix_keywords
+     ActiveSupport::Deprecation.warn "Setting.commit_fix_keywords is deprecated and will be removed in Redmine 3"
+    if commit_update_keywords.is_a?(Hash)
+      commit_update_keywords.keys.first
+    end
+  end
+
+  def self.commit_fix_status_id
+     ActiveSupport::Deprecation.warn "Setting.commit_fix_status_id is deprecated and will be removed in Redmine 3"
+    if commit_update_keywords.is_a?(Hash)
+      commit_update_keywords[commit_fix_keywords]['status_id']
+    end
+  end
+
+  def self.commit_fix_done_ratio
+     ActiveSupport::Deprecation.warn "Setting.commit_fix_done_ratio is deprecated and will be removed in Redmine 3"
+    if commit_update_keywords.is_a?(Hash)
+      commit_update_keywords[commit_fix_keywords]['done_ratio']
+    end
+  end
+
   def self.openid?
     Object.const_defined?(:OpenID) && self[:openid].to_i > 0
   end
index 3341de31342c069d7457dbc24cc4e9525591f124..d90c212ec61c68d9421041301fa5e8681a9cd629 100644 (file)
 <p><%= setting_text_field :commit_ref_keywords, :size => 30 %>
 <em class="info"><%= l(:text_comma_separated) %></em></p>
 
-<p><%= setting_text_field :commit_fix_keywords, :size => 30 %>
-&nbsp;<%= l(:label_applied_status) %>: <%= setting_select :commit_fix_status_id,
-                                                          [["", 0]] +
-                                                              IssueStatus.sorted.all.collect{
-                                                                 |status| [status.name, status.id.to_s]
-                                                              },
-                                                          :label => false %>
-&nbsp;<%= l(:field_done_ratio) %>: <%= setting_select :commit_fix_done_ratio,
-                                                       (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] },
-                                                       :blank => :label_no_change_option,
-                                                       :label => false %>
-<em class="info"><%= l(:text_comma_separated) %></em></p>
-
 <p><%= setting_check_box :commit_cross_project_ref %></p>
 
 <p><%= setting_check_box :commit_logtime_enabled,
                       :disabled => !Setting.commit_logtime_enabled?%></p>
 </fieldset>
 
-<%= submit_tag l(:button_save) %>
+<table class="list" id="commit-keywords">
+  <thead>
+    <tr>
+      <th><%= l(:setting_commit_fix_keywords) %></th>
+      <th><%= l(:label_applied_status) %></th>
+      <th><%= l(:field_done_ratio) %></th>
+      <th class="buttons"></th>
+    </tr>
+  </thead>
+  <tbody>
+    <% @commit_update_keywords.each do |keywords, updates| %>
+    <tr class="commit-keywords">
+      <td><%= text_field_tag "settings[commit_update_keywords][keywords][]", keywords, :size => 30 %></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><%= 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 class="buttons"><%= link_to image_tag('delete.png'), '#', :class => 'delete-commit-keywords' %></td>
+    </tr>
+    <% end %>
+    <tr>
+      <td><em class="info"><%= l(:text_comma_separated) %></em></td>
+      <td></td>
+      <td></td>
+      <td class="buttons"><%= link_to image_tag('add.png'), '#', :class => 'add-commit-keywords' %></td>
+    </tr>
+  </tbody>
+</table>
+
+<p><%= submit_tag l(:button_save) %></p>
+<% end %>
+
+<%= javascript_tag do %>
+$('#commit-keywords').on('click', 'a.delete-commit-keywords', function(e){
+  e.preventDefault();
+  if ($('#commit-keywords tbody tr.commit-keywords').length > 1) {
+    $(this).parents('#commit-keywords tr').remove();
+  } else {
+    $('#commit-keywords tbody tr.commit-keywords').find('input, select').val('');
+  }
+});
+$('#commit-keywords').on('click', 'a.add-commit-keywords', function(e){
+  e.preventDefault();
+  var row = $('#commit-keywords tr.commit-keywords:last');
+  row.clone().insertAfter(row).find('input, select').val('');
+});
 <% end %>
index 61c24c7980298f44e42164bc876ff23d97402f35..05779acccf46b2a8bfb2cf0f881602cb3d0ab97b 100644 (file)
@@ -106,13 +106,9 @@ commit_cross_project_ref:
   default: 0
 commit_ref_keywords:
   default: 'refs,references,IssueID'
-commit_fix_keywords:
-  default: 'fixes,closes'
-commit_fix_status_id:
-  format: int
-  default: 0
-commit_fix_done_ratio:
-  default: 100
+commit_update_keywords:
+  serialized: true
+  default: {}
 commit_logtime_enabled:
   default: 0
 commit_logtime_activity_id:
diff --git a/db/migrate/20131004113137_support_for_multiple_commit_keywords.rb b/db/migrate/20131004113137_support_for_multiple_commit_keywords.rb
new file mode 100644 (file)
index 0000000..77989d9
--- /dev/null
@@ -0,0 +1,17 @@
+class SupportForMultipleCommitKeywords < ActiveRecord::Migration
+  def up
+    # Replaces commit_fix_keywords, commit_fix_status_id, commit_fix_done_ratio settings
+    # with commit_update_keywords setting
+    keywords = Setting.where(:name => 'commit_fix_keywords').limit(1).pluck(:value).first
+    status_id = Setting.where(:name => 'commit_fix_status_id').limit(1).pluck(:value).first
+    done_ratio = Setting.where(:name => 'commit_fix_done_ratio').limit(1).pluck(:value).first
+    if keywords.present?
+      Setting.commit_update_keywords = {keywords => {'status_id' => status_id, 'done_ratio' => done_ratio}}
+    end
+    Setting.where(:name => %w(commit_fix_keywords commit_fix_status_id commit_fix_done_ratio)).delete_all
+  end
+
+  def down
+    Setting.where(:name => 'commit_update_keywords').delete_all
+  end
+end
index df4abd6b74cc2dfbad525c3f047e77645058ba3e..57dd629b2db299d2c1c449e5a1f7b2cfe52dccd7 100644 (file)
@@ -80,6 +80,57 @@ class SettingsControllerTest < ActionController::TestCase
     Setting.clear_cache
   end
 
+  def test_edit_commit_update_keywords
+    with_settings :commit_update_keywords => {
+      "fixes, resolves" => {"status_id" => "3"},
+      "closes" => {"status_id" => "5", "done_ratio" => "100"}
+    } do
+      get :edit
+    end
+    assert_response :success
+    assert_select 'tr.commit-keywords', 2
+    assert_select 'tr.commit-keywords:nth-child(1)' do
+      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 'option[value=3][selected=selected]'
+      end
+    end
+    assert_select 'tr.commit-keywords:nth-child(2)' do
+      assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
+      assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
+        assert_select 'option[value=5][selected=selected]'
+      end
+      assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
+        assert_select 'option[value=100][selected=selected]'
+      end
+    end
+  end
+
+  def test_edit_without_commit_update_keywords_should_show_blank_line
+    with_settings :commit_update_keywords => {} do
+      get :edit
+    end
+    assert_response :success
+    assert_select 'tr.commit-keywords', 1 do
+      assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', ''
+    end
+  end
+
+  def test_post_edit_commit_update_keywords
+    post :edit, :settings => {
+      :commit_update_keywords => {
+        :keywords => ["resolves", "closes"],
+        :status_id => ["3", "5"],
+        :done_ratio => ["", "100"]
+      }
+    }
+    assert_redirected_to '/settings'
+    assert_equal({
+      "resolves" => {"status_id" => "3"},
+      "closes" => {"status_id" => "5", "done_ratio" => "100"}
+    }, Setting.commit_update_keywords)
+  end
+
   def test_get_plugin_settings
     Setting.stubs(:plugin_foo).returns({'sample_setting' => 'Plugin setting value'})
     ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins"))
index a97dc37d22b4148f650221e8dd23777675f6d39b..3ead6b8436dd126105f4f551a28ac22b93afeff4 100644 (file)
@@ -30,10 +30,8 @@ class ChangesetTest < ActiveSupport::TestCase
 
   def test_ref_keywords_any
     ActionMailer::Base.deliveries.clear
-    Setting.commit_fix_status_id = IssueStatus.where(:is_closed => true).first.id
-    Setting.commit_fix_done_ratio = '90'
     Setting.commit_ref_keywords = '*'
-    Setting.commit_fix_keywords = 'fixes , closes'
+    Setting.commit_update_keywords = {'fixes , closes' => {'status_id' => '5', 'done_ratio' => '90'}}
 
     c = Changeset.new(:repository   => Project.find(1).repository,
                       :committed_on => Time.now,
@@ -49,7 +47,7 @@ class ChangesetTest < ActiveSupport::TestCase
 
   def test_ref_keywords
     Setting.commit_ref_keywords = 'refs'
-    Setting.commit_fix_keywords = ''
+    Setting.commit_update_keywords = ''
     c = Changeset.new(:repository   => Project.find(1).repository,
                       :committed_on => Time.now,
                       :comments     => 'Ignores #2. Refs #1',
@@ -60,7 +58,7 @@ class ChangesetTest < ActiveSupport::TestCase
 
   def test_ref_keywords_any_only
     Setting.commit_ref_keywords = '*'
-    Setting.commit_fix_keywords = ''
+    Setting.commit_update_keywords = ''
     c = Changeset.new(:repository   => Project.find(1).repository,
                       :committed_on => Time.now,
                       :comments     => 'Ignores #2. Refs #1',
@@ -112,9 +110,12 @@ class ChangesetTest < ActiveSupport::TestCase
   end
 
   def test_ref_keywords_closing_with_timelog
-    Setting.commit_fix_status_id = IssueStatus.where(:is_closed => true).first.id
     Setting.commit_ref_keywords = '*'
-    Setting.commit_fix_keywords = 'fixes , closes'
+    Setting.commit_update_keywords = {
+      'fixes , closes' => {
+        'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s
+      }
+    }
     Setting.commit_logtime_enabled = '1'
 
     c = Changeset.new(:repository   => Project.find(1).repository,
@@ -163,6 +164,23 @@ class ChangesetTest < ActiveSupport::TestCase
     assert_equal [1,2,3], c.issue_ids.sort
   end
 
+  def test_update_keywords_with_multiple_rules
+    Setting.commit_update_keywords = {
+      'fixes, closes' => {'status_id' => '5'},
+      'resolves' => {'status_id' => '3'}
+    }
+    issue1 = Issue.generate!
+    issue2 = Issue.generate!
+
+    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 3, issue2.reload.status_id
+  end
+
   def test_commit_referencing_a_subproject_issue
     c = Changeset.new(:repository   => Project.find(1).repository,
                       :committed_on => Time.now,
@@ -174,7 +192,7 @@ class ChangesetTest < ActiveSupport::TestCase
   end
 
   def test_commit_closing_a_subproject_issue
-    with_settings :commit_fix_status_id => 5, :commit_fix_keywords => 'closes',
+    with_settings :commit_update_keywords => {'closes' => {'status_id' => '5'}},
                   :default_language => 'en' do
       issue = Issue.find(5)
       assert !issue.closed?
index 390a5334e2f6fb9be6d5e6f4aff5dd74d13d8601..3ff9fc51ecc4fd4c431c303ab9f9a8428ad7091e 100644 (file)
@@ -182,11 +182,10 @@ class RepositoryTest < ActiveSupport::TestCase
   def test_scan_changesets_for_issue_ids
     Setting.default_language = 'en'
 
-    # choosing a status to apply to fix issues
-    Setting.commit_fix_status_id = IssueStatus.where(:is_closed => true).first.id
-    Setting.commit_fix_done_ratio = "90"
     Setting.commit_ref_keywords = 'refs , references, IssueID'
-    Setting.commit_fix_keywords = 'fixes , closes'
+    Setting.commit_update_keywords = {
+      'fixes , closes' => {'status_id' => IssueStatus.where(:is_closed => true).first.id, 'done_ratio' => '90'}
+    }
     Setting.default_language = 'en'
     ActionMailer::Base.deliveries.clear