Keywords and the status to apply to fixed issues can be defined in Admin -> Settings. Default keywords: - for referencing issues: refs, references, IssueID - for fixing issues: fixes,closes There's no default status defined for fixed issue. You'll have to specify it if you want to enable auto closure of issues. Example of a working commit message: "This commit references #1, #2 and fixes #3" git-svn-id: http://redmine.rubyforge.org/svn/trunk@473 e93f8b46-1217-0410-a6f0-8f06a7374b81tags/0.5.1
@@ -18,6 +18,7 @@ | |||
class Changeset < ActiveRecord::Base | |||
belongs_to :repository | |||
has_many :changes, :dependent => :delete_all | |||
has_and_belongs_to_many :issues | |||
validates_presence_of :repository_id, :revision, :committed_on, :commit_date | |||
validates_numericality_of :revision, :only_integer => true | |||
@@ -27,4 +28,41 @@ class Changeset < ActiveRecord::Base | |||
self.commit_date = date | |||
super | |||
end | |||
def after_create | |||
scan_comment_for_issue_ids | |||
end | |||
def scan_comment_for_issue_ids | |||
return if comment.blank? | |||
# keywords used to reference issues | |||
ref_keywords = Setting.commit_ref_keywords.downcase.split(",") | |||
# keywords used to fix issues | |||
fix_keywords = Setting.commit_fix_keywords.downcase.split(",") | |||
# status applied | |||
fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id) | |||
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw.strip)}.join("|") | |||
return if kw_regexp.blank? | |||
# remove any associated issues | |||
self.issues.clear | |||
comment.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match| | |||
action = match[0] | |||
target_issue_ids = match[1].scan(/\d+/) | |||
target_issues = repository.project.issues.find_all_by_id(target_issue_ids) | |||
if fix_status && fix_keywords.include?(action.downcase) | |||
# update status of issues | |||
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug? | |||
target_issues.each do |issue| | |||
# don't change the status is the issue is already closed | |||
next if issue.status.is_closed? | |||
issue.status = fix_status | |||
issue.save | |||
end | |||
end | |||
self.issues << target_issues | |||
end | |||
end | |||
end |
@@ -31,7 +31,8 @@ class Issue < ActiveRecord::Base | |||
has_many :time_entries | |||
has_many :custom_values, :dependent => :delete_all, :as => :customized | |||
has_many :custom_fields, :through => :custom_values | |||
has_and_belongs_to_many :changesets, :order => "revision ASC" | |||
acts_as_watchable | |||
validates_presence_of :subject, :description, :priority, :tracker, :author, :status |
@@ -79,10 +79,19 @@ class Repository < ActiveRecord::Base | |||
end | |||
end | |||
def scan_changesets_for_issue_ids | |||
self.changesets.each(&:scan_comment_for_issue_ids) | |||
end | |||
# fetch new changesets for all repositories | |||
# can be called periodically by an external script | |||
# eg. ruby script/runner "Repository.fetch_changesets" | |||
def self.fetch_changesets | |||
find(:all).each(&:fetch_changesets) | |||
end | |||
# scan changeset comments to find related and fixed issues for all repositories | |||
def self.scan_changesets_for_issue_ids | |||
find(:all).each(&:scan_changesets_for_issue_ids) | |||
end | |||
end |
@@ -44,7 +44,12 @@ end %> | |||
</tr> | |||
</table> | |||
<hr /> | |||
<br /> | |||
<% if @issue.changesets.any? %> | |||
<div style="float:right;"> | |||
<em><%= l(:label_revision_plural) %>: <%= @issue.changesets.collect{|changeset| link_to(changeset.revision, :controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision)}.join(", ") %></em> | |||
</div> | |||
<% end %> | |||
<b><%=l(:field_description)%> :</b><br /><br /> | |||
<%= textilizable @issue.description %> |
@@ -10,6 +10,15 @@ | |||
<p><em><%= @changeset.committer %>, <%= format_time(@changeset.committed_on) %></em></p> | |||
<%= textilizable @changeset.comment %> | |||
<% if @changeset.issues.any? %> | |||
<h3><%= l(:label_related_issues) %></h3> | |||
<ul> | |||
<% @changeset.issues.each do |issue| %> | |||
<li><%= link_to_issue issue %>: <%=h issue.subject %></li> | |||
<% end %> | |||
</ul> | |||
<% end %> | |||
<div style="float:right;"> | |||
<div class="square action_A"></div> <div style="float:left;"><%= l(:label_added) %> </div> | |||
<div class="square action_M"></div> <div style="float:left;"><%= l(:label_modified) %> </div> |
@@ -50,8 +50,18 @@ | |||
<p><label><%= l(:setting_sys_api_enabled) %></label> | |||
<%= check_box_tag 'settings[sys_api_enabled]', 1, Setting.sys_api_enabled? %><%= hidden_field_tag 'settings[sys_api_enabled]', 0 %></p> | |||
</div> | |||
<fieldset class="box"><legend><%= l(:text_issues_ref_in_commit_messages) %></legend> | |||
<p><label><%= l(:setting_commit_ref_keywords) %></label> | |||
<%= text_field_tag 'settings[commit_ref_keywords]', Setting.commit_ref_keywords, :size => 30 %><br /><em><%= l(:text_coma_separated) %></em></p> | |||
<p><label><%= l(:setting_commit_fix_keywords) %></label> | |||
<%= text_field_tag 'settings[commit_fix_keywords]', Setting.commit_fix_keywords, :size => 30 %> | |||
<%= l(:label_applied_status) %>: <%= select_tag 'settings[commit_fix_status_id]', options_for_select( [["", 0]] + IssueStatus.find(:all).collect{|status| [status.name, status.id.to_s]}, Setting.commit_fix_status_id) %> | |||
<br /><em><%= l(:text_coma_separated) %></em></p> | |||
</fieldset> | |||
<%= submit_tag l(:button_save) %> | |||
</div> | |||
<% end %> |
@@ -54,3 +54,11 @@ autofetch_changesets: | |||
default: 1 | |||
sys_api_enabled: | |||
default: 0 | |||
commit_ref_keywords: | |||
default: 'refs,references,IssueID' | |||
commit_fix_keywords: | |||
default: 'fixes,closes' | |||
commit_fix_status_id: | |||
format: int | |||
default: 0 | |||
@@ -0,0 +1,13 @@ | |||
class CreateChangesetsIssues < ActiveRecord::Migration | |||
def self.up | |||
create_table :changesets_issues, :id => false do |t| | |||
t.column :changeset_id, :integer, :null => false | |||
t.column :issue_id, :integer, :null => false | |||
end | |||
add_index :changesets_issues, [:changeset_id, :issue_id], :unique => true, :name => :changesets_issues_ids | |||
end | |||
def self.down | |||
drop_table :changesets_issues | |||
end | |||
end |
@@ -164,6 +164,8 @@ setting_wiki_compression: Wiki-Historie komprimieren | |||
setting_feeds_limit: Limit Feed Inhalt | |||
setting_autofetch_changesets: Autofetch SVN commits | |||
setting_sys_api_enabled: Enable WS for repository management | |||
setting_commit_ref_keywords: Referencing keywords | |||
setting_commit_fix_keywords: Fixing keywords | |||
label_user: Benutzer | |||
label_user_plural: Benutzer | |||
@@ -358,6 +360,8 @@ label_options: Options | |||
label_copy_workflow_from: Copy workflow from | |||
label_permissions_report: Permissions report | |||
label_watched_issues: Watched issues | |||
label_related_issues: Related issues | |||
label_applied_status: Applied status | |||
button_login: Einloggen | |||
button_submit: OK | |||
@@ -408,6 +412,8 @@ text_caracters_maximum: %d characters maximum. | |||
text_length_between: Length between %d and %d characters. | |||
text_tracker_no_workflow: No workflow defined for this tracker | |||
text_unallowed_characters: Unallowed characters | |||
text_coma_separated: Multiple values allowed (coma separated). | |||
text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages | |||
default_role_manager: Manager | |||
default_role_developper: Developer |
@@ -164,6 +164,8 @@ setting_wiki_compression: Wiki history compression | |||
setting_feeds_limit: Feed content limit | |||
setting_autofetch_changesets: Autofetch SVN commits | |||
setting_sys_api_enabled: Enable WS for repository management | |||
setting_commit_ref_keywords: Referencing keywords | |||
setting_commit_fix_keywords: Fixing keywords | |||
label_user: User | |||
label_user_plural: Users | |||
@@ -358,6 +360,8 @@ label_options: Options | |||
label_copy_workflow_from: Copy workflow from | |||
label_permissions_report: Permissions report | |||
label_watched_issues: Watched issues | |||
label_related_issues: Related issues | |||
label_applied_status: Applied status | |||
button_login: Login | |||
button_submit: Submit | |||
@@ -408,6 +412,8 @@ text_caracters_maximum: %d characters maximum. | |||
text_length_between: Length between %d and %d characters. | |||
text_tracker_no_workflow: No workflow defined for this tracker | |||
text_unallowed_characters: Unallowed characters | |||
text_coma_separated: Multiple values allowed (coma separated). | |||
text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages | |||
default_role_manager: Manager | |||
default_role_developper: Developer |
@@ -164,6 +164,8 @@ setting_wiki_compression: Compresión de la historia de Wiki | |||
setting_feeds_limit: Feed content limit | |||
setting_autofetch_changesets: Autofetch SVN commits | |||
setting_sys_api_enabled: Enable WS for repository management | |||
setting_commit_ref_keywords: Referencing keywords | |||
setting_commit_fix_keywords: Fixing keywords | |||
label_user: Usuario | |||
label_user_plural: Usuarios | |||
@@ -358,6 +360,8 @@ label_options: Options | |||
label_copy_workflow_from: Copy workflow from | |||
label_permissions_report: Permissions report | |||
label_watched_issues: Watched issues | |||
label_related_issues: Related issues | |||
label_applied_status: Applied status | |||
button_login: Conexión | |||
button_submit: Someter | |||
@@ -408,6 +412,8 @@ text_caracters_maximum: %d characters maximum. | |||
text_length_between: Length between %d and %d characters. | |||
text_tracker_no_workflow: No workflow defined for this tracker | |||
text_unallowed_characters: Unallowed characters | |||
text_coma_separated: Multiple values allowed (coma separated). | |||
text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages | |||
default_role_manager: Manager | |||
default_role_developper: Desarrollador |
@@ -164,6 +164,8 @@ setting_wiki_compression: Compression historique wiki | |||
setting_feeds_limit: Limite du contenu des flux RSS | |||
setting_autofetch_changesets: Récupération auto. des commits SVN | |||
setting_sys_api_enabled: Activer les WS pour la gestion des dépôts | |||
setting_commit_ref_keywords: Mot-clés de référencement | |||
setting_commit_fix_keywords: Mot-clés de résolution | |||
label_user: Utilisateur | |||
label_user_plural: Utilisateurs | |||
@@ -358,6 +360,8 @@ label_options: Options | |||
label_copy_workflow_from: Copier le workflow de | |||
label_permissions_report: Synthèse des permissions | |||
label_watched_issues: Demandes surveillées | |||
label_related_issues: Demandes liées | |||
label_applied_status: Statut appliqué | |||
button_login: Connexion | |||
button_submit: Soumettre | |||
@@ -408,6 +412,8 @@ text_caracters_maximum: %d caractères maximum. | |||
text_length_between: Longueur comprise entre %d et %d caractères. | |||
text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker | |||
text_unallowed_characters: Caractères non autorisés | |||
text_coma_separated: Plusieurs valeurs possibles (séparées par des virgules). | |||
text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires SVN | |||
default_role_manager: Manager | |||
default_role_developper: Développeur |
@@ -164,6 +164,8 @@ setting_wiki_compression: Compressione di storia di Wiki | |||
setting_feeds_limit: Limite contenuti del feed | |||
setting_autofetch_changesets: Acquisisci automaticamente le commit SVN | |||
setting_sys_api_enabled: Abilita WS per la gestione del repository | |||
setting_commit_ref_keywords: Referencing keywords | |||
setting_commit_fix_keywords: Fixing keywords | |||
label_user: Utente | |||
label_user_plural: Utenti | |||
@@ -358,6 +360,8 @@ label_options: Opzioni | |||
label_copy_workflow_from: Copia workflow da | |||
label_permissions_report: Report permessi | |||
label_watched_issues: Watched issues | |||
label_related_issues: Related issues | |||
label_applied_status: Applied status | |||
button_login: Login | |||
button_submit: Invia | |||
@@ -408,6 +412,8 @@ text_caracters_maximum: massimo %d caratteri. | |||
text_length_between: Lunghezza compresa tra %d e %d caratteri. | |||
text_tracker_no_workflow: Nessun workflow definito per questo tracker | |||
text_unallowed_characters: Unallowed characters | |||
text_coma_separated: Multiple values allowed (coma separated). | |||
text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages | |||
default_role_manager: Manager | |||
default_role_developper: Sviluppatore |
@@ -165,6 +165,8 @@ setting_wiki_compression: Wiki履歴を圧縮する | |||
setting_feeds_limit: フィード内容の上限 | |||
setting_autofetch_changesets: SVNコミットを自動取得する | |||
setting_sys_api_enabled: リポジトリ管理用のWeb Serviceを有効化する | |||
setting_commit_ref_keywords: Referencing keywords | |||
setting_commit_fix_keywords: Fixing keywords | |||
label_user: ユーザ | |||
label_user_plural: ユーザ | |||
@@ -359,6 +361,8 @@ label_options: オプション | |||
label_copy_workflow_from: ワークフローをここからコピー | |||
label_permissions_report: 権限レポート | |||
label_watched_issues: Watched issues | |||
label_related_issues: Related issues | |||
label_applied_status: Applied status | |||
button_login: ログイン | |||
button_submit: 変更 | |||
@@ -409,6 +413,8 @@ text_caracters_maximum: 最大 %d 文字です。 | |||
text_length_between: 長さは %d から %d 文字までです。 | |||
text_tracker_no_workflow: このトラッカーにワークフローが定義されていません | |||
text_unallowed_characters: Unallowed characters | |||
text_coma_separated: Multiple values allowed (coma separated). | |||
text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages | |||
default_role_manager: 管理者 | |||
default_role_developper: 開発者 |
@@ -164,6 +164,8 @@ setting_wiki_compression: Compactacao do historio do Wiki | |||
setting_feeds_limit: Limite do Feed | |||
setting_autofetch_changesets: Autofetch SVN commits | |||
setting_sys_api_enabled: Ativa WS para gerenciamento do repositorio | |||
setting_commit_ref_keywords: Referencing keywords | |||
setting_commit_fix_keywords: Fixing keywords | |||
label_user: Usuario | |||
label_user_plural: Usuarios | |||
@@ -358,6 +360,8 @@ label_options: Opcoes | |||
label_copy_workflow_from: Copiar workflow de | |||
label_permissions_report: Relatorio de permissoes | |||
label_watched_issues: Watched issues | |||
label_related_issues: Related issues | |||
label_applied_status: Applied status | |||
button_login: Login | |||
button_submit: Enviar | |||
@@ -408,6 +412,8 @@ text_caracters_maximum: %d maximo de caracteres | |||
text_length_between: Tamanho entre %d e %d caracteres. | |||
text_tracker_no_workflow: Sem workflow definido para este tipo. | |||
text_unallowed_characters: Unallowed characters | |||
text_coma_separated: Multiple values allowed (coma separated). | |||
text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages | |||
default_role_manager: Analista de Negocio ou Gerente de Projeto | |||
default_role_developper: Desenvolvedor |
@@ -167,6 +167,8 @@ setting_wiki_compression: Wiki history compression | |||
setting_feeds_limit: Feed content limit | |||
setting_autofetch_changesets: Autofetch SVN commits | |||
setting_sys_api_enabled: Enable WS for repository management | |||
setting_commit_ref_keywords: Referencing keywords | |||
setting_commit_fix_keywords: Fixing keywords | |||
label_user: 用户 | |||
label_user_plural: 用户列表 | |||
@@ -361,6 +363,8 @@ label_options: Options | |||
label_copy_workflow_from: Copy workflow from | |||
label_permissions_report: Permissions report | |||
label_watched_issues: Watched issues | |||
label_related_issues: Related issues | |||
label_applied_status: Applied status | |||
button_login: 登录 | |||
button_submit: 提交 | |||
@@ -411,6 +415,8 @@ text_caracters_maximum: %d characters maximum. | |||
text_length_between: Length between %d and %d characters. | |||
text_tracker_no_workflow: No workflow defined for this tracker | |||
text_unallowed_characters: Unallowed characters | |||
text_coma_separated: Multiple values allowed (coma separated). | |||
text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages | |||
default_role_manager: 管理员 | |||
default_role_developper: 开发人员 |
@@ -602,8 +602,8 @@ margin*/ | |||
color: #cc0000; | |||
} | |||
#settings .tabular p{ padding-left: 250px; } | |||
#settings .tabular label{ margin-left: -250px; width: 245px; } | |||
#settings .tabular p{ padding-left: 300px; } | |||
#settings .tabular label{ margin-left: -300px; width: 295px; } | |||
/*.threepxfix class below: | |||
Targets IE6- ONLY. Adds 3 pixel indent for multi-line form contents. |
@@ -0,0 +1,38 @@ | |||
--- | |||
changesets_001: | |||
commit_date: 2007-04-11 | |||
committed_on: 2007-04-11 15:14:44 +02:00 | |||
revision: 1 | |||
id: 100 | |||
comment: My very first commit | |||
repository_id: 10 | |||
committer: dlopper | |||
changesets_002: | |||
commit_date: 2007-04-12 | |||
committed_on: 2007-04-12 15:14:44 +02:00 | |||
revision: 2 | |||
id: 101 | |||
comment: 'This commit fixes #1, #2 and references #3' | |||
repository_id: 10 | |||
committer: dlopper | |||
changesets_003: | |||
commit_date: 2007-04-12 | |||
committed_on: 2007-04-12 15:14:44 +02:00 | |||
revision: 3 | |||
id: 102 | |||
comment: |- | |||
A commit with wrong issue ids | |||
IssueID 666 3 | |||
repository_id: 10 | |||
committer: dlopper | |||
changesets_004: | |||
commit_date: 2007-04-12 | |||
committed_on: 2007-04-12 15:14:44 +02:00 | |||
revision: 4 | |||
id: 103 | |||
comment: |- | |||
A commit with an issue id of an other project | |||
IssueID 4 2 | |||
repository_id: 10 | |||
committer: dlopper | |||
@@ -41,3 +41,18 @@ issues_003: | |||
assigned_to_id: | |||
author_id: 2 | |||
status_id: 1 | |||
issues_004: | |||
created_on: 2006-07-19 21:07:27 +02:00 | |||
project_id: 2 | |||
updated_on: 2006-07-19 21:07:27 +02:00 | |||
priority_id: 4 | |||
subject: Issue on project 2 | |||
id: 4 | |||
fixed_version_id: | |||
category_id: | |||
description: Issue on project 2 | |||
tracker_id: 1 | |||
assigned_to_id: | |||
author_id: 2 | |||
status_id: 1 | |||
@@ -0,0 +1,8 @@ | |||
--- | |||
repositories_001: | |||
project_id: 1 | |||
url: svn://localhost/test | |||
id: 10 | |||
root_url: svn://localhost | |||
password: "" | |||
login: "" |
@@ -0,0 +1,59 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006-2007 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 2 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
require File.dirname(__FILE__) + '/../test_helper' | |||
class RepositoryTest < Test::Unit::TestCase | |||
fixtures :projects, :repositories, :issues, :issue_statuses, :changesets | |||
def test_create | |||
repository = Repository.new(:project => Project.find(2)) | |||
assert !repository.save | |||
repository.url = "svn://localhost" | |||
assert repository.save | |||
repository.reload | |||
project = Project.find(2) | |||
assert_equal repository, project.repository | |||
end | |||
def test_cant_change_url | |||
repository = Project.find(1).repository | |||
url = repository.url | |||
repository.url = "svn://anotherhost" | |||
assert_equal url, repository.url | |||
end | |||
def test_scan_changesets_for_issue_ids | |||
# choosing a status to apply to fix issues | |||
Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id | |||
# make sure issue 1 is not already closed | |||
assert !Issue.find(1).status.is_closed? | |||
Repository.scan_changesets_for_issue_ids | |||
assert_equal [101, 102], Issue.find(3).changeset_ids | |||
# fixed issues | |||
assert Issue.find(1).status.is_closed? | |||
assert_equal [101], Issue.find(1).changeset_ids | |||
# ignoring commits referencing an issue of another project | |||
assert_equal [], Issue.find(4).changesets | |||
end | |||
end |