##// END OF EJS Templates
Allow referencing issue numbers in brackets. This style is used by other...
Eric Davis -
r2749:609faba6a3d3
parent child
Show More
@@ -1,170 +1,170
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'iconv'
18 require 'iconv'
19
19
20 class Changeset < ActiveRecord::Base
20 class Changeset < ActiveRecord::Base
21 belongs_to :repository
21 belongs_to :repository
22 belongs_to :user
22 belongs_to :user
23 has_many :changes, :dependent => :delete_all
23 has_many :changes, :dependent => :delete_all
24 has_and_belongs_to_many :issues
24 has_and_belongs_to_many :issues
25
25
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 :description => :long_comments,
27 :description => :long_comments,
28 :datetime => :committed_on,
28 :datetime => :committed_on,
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
30
30
31 acts_as_searchable :columns => 'comments',
31 acts_as_searchable :columns => 'comments',
32 :include => {:repository => :project},
32 :include => {:repository => :project},
33 :project_key => "#{Repository.table_name}.project_id",
33 :project_key => "#{Repository.table_name}.project_id",
34 :date_column => 'committed_on'
34 :date_column => 'committed_on'
35
35
36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 :author_key => :user_id,
37 :author_key => :user_id,
38 :find_options => {:include => [:user, {:repository => :project}]}
38 :find_options => {:include => [:user, {:repository => :project}]}
39
39
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 validates_uniqueness_of :revision, :scope => :repository_id
41 validates_uniqueness_of :revision, :scope => :repository_id
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43
43
44 def revision=(r)
44 def revision=(r)
45 write_attribute :revision, (r.nil? ? nil : r.to_s)
45 write_attribute :revision, (r.nil? ? nil : r.to_s)
46 end
46 end
47
47
48 def comments=(comment)
48 def comments=(comment)
49 write_attribute(:comments, Changeset.normalize_comments(comment))
49 write_attribute(:comments, Changeset.normalize_comments(comment))
50 end
50 end
51
51
52 def committed_on=(date)
52 def committed_on=(date)
53 self.commit_date = date
53 self.commit_date = date
54 super
54 super
55 end
55 end
56
56
57 def project
57 def project
58 repository.project
58 repository.project
59 end
59 end
60
60
61 def author
61 def author
62 user || committer.to_s.split('<').first
62 user || committer.to_s.split('<').first
63 end
63 end
64
64
65 def before_create
65 def before_create
66 self.user = repository.find_committer_user(committer)
66 self.user = repository.find_committer_user(committer)
67 end
67 end
68
68
69 def after_create
69 def after_create
70 scan_comment_for_issue_ids
70 scan_comment_for_issue_ids
71 end
71 end
72 require 'pp'
72 require 'pp'
73
73
74 def scan_comment_for_issue_ids
74 def scan_comment_for_issue_ids
75 return if comments.blank?
75 return if comments.blank?
76 # keywords used to reference issues
76 # keywords used to reference issues
77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
78 # keywords used to fix issues
78 # keywords used to fix issues
79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
80 # status and optional done ratio applied
80 # status and optional done ratio applied
81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
82 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
82 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
83
83
84 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
84 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
85 return if kw_regexp.blank?
85 return if kw_regexp.blank?
86
86
87 referenced_issues = []
87 referenced_issues = []
88
88
89 if ref_keywords.delete('*')
89 if ref_keywords.delete('*')
90 # find any issue ID in the comments
90 # find any issue ID in the comments
91 target_issue_ids = []
91 target_issue_ids = []
92 comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
92 comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
93 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
93 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
94 end
94 end
95
95
96 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
96 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
97 action = match[0]
97 action = match[0]
98 target_issue_ids = match[1].scan(/\d+/)
98 target_issue_ids = match[1].scan(/\d+/)
99 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
99 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
100 if fix_status && fix_keywords.include?(action.downcase)
100 if fix_status && fix_keywords.include?(action.downcase)
101 # update status of issues
101 # update status of issues
102 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
102 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
103 target_issues.each do |issue|
103 target_issues.each do |issue|
104 # the issue may have been updated by the closure of another one (eg. duplicate)
104 # the issue may have been updated by the closure of another one (eg. duplicate)
105 issue.reload
105 issue.reload
106 # don't change the status is the issue is closed
106 # don't change the status is the issue is closed
107 next if issue.status.is_closed?
107 next if issue.status.is_closed?
108 csettext = "r#{self.revision}"
108 csettext = "r#{self.revision}"
109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
110 csettext = "commit:\"#{self.scmid}\""
110 csettext = "commit:\"#{self.scmid}\""
111 end
111 end
112 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
112 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
113 issue.status = fix_status
113 issue.status = fix_status
114 issue.done_ratio = done_ratio if done_ratio
114 issue.done_ratio = done_ratio if done_ratio
115 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
115 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
116 { :changeset => self, :issue => issue })
116 { :changeset => self, :issue => issue })
117 issue.save
117 issue.save
118 end
118 end
119 end
119 end
120 referenced_issues += target_issues
120 referenced_issues += target_issues
121 end
121 end
122
122
123 self.issues = referenced_issues.uniq
123 self.issues = referenced_issues.uniq
124 end
124 end
125
125
126 def short_comments
126 def short_comments
127 @short_comments || split_comments.first
127 @short_comments || split_comments.first
128 end
128 end
129
129
130 def long_comments
130 def long_comments
131 @long_comments || split_comments.last
131 @long_comments || split_comments.last
132 end
132 end
133
133
134 # Returns the previous changeset
134 # Returns the previous changeset
135 def previous
135 def previous
136 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
136 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
137 end
137 end
138
138
139 # Returns the next changeset
139 # Returns the next changeset
140 def next
140 def next
141 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
141 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
142 end
142 end
143
143
144 # Strips and reencodes a commit log before insertion into the database
144 # Strips and reencodes a commit log before insertion into the database
145 def self.normalize_comments(str)
145 def self.normalize_comments(str)
146 to_utf8(str.to_s.strip)
146 to_utf8(str.to_s.strip)
147 end
147 end
148
148
149 private
149 private
150
150
151 def split_comments
151 def split_comments
152 comments =~ /\A(.+?)\r?\n(.*)$/m
152 comments =~ /\A(.+?)\r?\n(.*)$/m
153 @short_comments = $1 || comments
153 @short_comments = $1 || comments
154 @long_comments = $2.to_s.strip
154 @long_comments = $2.to_s.strip
155 return @short_comments, @long_comments
155 return @short_comments, @long_comments
156 end
156 end
157
157
158 def self.to_utf8(str)
158 def self.to_utf8(str)
159 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
159 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
160 encoding = Setting.commit_logs_encoding.to_s.strip
160 encoding = Setting.commit_logs_encoding.to_s.strip
161 unless encoding.blank? || encoding == 'UTF-8'
161 unless encoding.blank? || encoding == 'UTF-8'
162 begin
162 begin
163 return Iconv.conv('UTF-8', encoding, str)
163 return Iconv.conv('UTF-8', encoding, str)
164 rescue Iconv::Failure
164 rescue Iconv::Failure
165 # do nothing here
165 # do nothing here
166 end
166 end
167 end
167 end
168 str
168 str
169 end
169 end
170 end
170 end
@@ -1,75 +1,97
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class ChangesetTest < Test::Unit::TestCase
20 class ChangesetTest < Test::Unit::TestCase
21 fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :member_roles, :trackers
21 fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :member_roles, :trackers
22
22
23 def setup
23 def setup
24 end
24 end
25
25
26 def test_ref_keywords_any
26 def test_ref_keywords_any
27 ActionMailer::Base.deliveries.clear
27 ActionMailer::Base.deliveries.clear
28 Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id
28 Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id
29 Setting.commit_fix_done_ratio = '90'
29 Setting.commit_fix_done_ratio = '90'
30 Setting.commit_ref_keywords = '*'
30 Setting.commit_ref_keywords = '*'
31 Setting.commit_fix_keywords = 'fixes , closes'
31 Setting.commit_fix_keywords = 'fixes , closes'
32
32
33 c = Changeset.new(:repository => Project.find(1).repository,
33 c = Changeset.new(:repository => Project.find(1).repository,
34 :committed_on => Time.now,
34 :committed_on => Time.now,
35 :comments => 'New commit (#2). Fixes #1')
35 :comments => 'New commit (#2). Fixes #1')
36 c.scan_comment_for_issue_ids
36 c.scan_comment_for_issue_ids
37
37
38 assert_equal [1, 2], c.issue_ids.sort
38 assert_equal [1, 2], c.issue_ids.sort
39 fixed = Issue.find(1)
39 fixed = Issue.find(1)
40 assert fixed.closed?
40 assert fixed.closed?
41 assert_equal 90, fixed.done_ratio
41 assert_equal 90, fixed.done_ratio
42 assert_equal 1, ActionMailer::Base.deliveries.size
42 assert_equal 1, ActionMailer::Base.deliveries.size
43 end
43 end
44
44
45 def test_ref_keywords_any_line_start
45 def test_ref_keywords_any_line_start
46 Setting.commit_ref_keywords = '*'
46 Setting.commit_ref_keywords = '*'
47
47
48 c = Changeset.new(:repository => Project.find(1).repository,
48 c = Changeset.new(:repository => Project.find(1).repository,
49 :committed_on => Time.now,
49 :committed_on => Time.now,
50 :comments => '#1 is the reason of this commit')
50 :comments => '#1 is the reason of this commit')
51 c.scan_comment_for_issue_ids
51 c.scan_comment_for_issue_ids
52
52
53 assert_equal [1], c.issue_ids.sort
53 assert_equal [1], c.issue_ids.sort
54 end
54 end
55
55
56 def test_ref_keywords_allow_brackets_around_a_issue_number
57 Setting.commit_ref_keywords = '*'
58
59 c = Changeset.new(:repository => Project.find(1).repository,
60 :committed_on => Time.now,
61 :comments => '[#1] Worked on this issue')
62 c.scan_comment_for_issue_ids
63
64 assert_equal [1], c.issue_ids.sort
65 end
66
67 def test_ref_keywords_allow_brackets_around_multiple_issue_numbers
68 Setting.commit_ref_keywords = '*'
69
70 c = Changeset.new(:repository => Project.find(1).repository,
71 :committed_on => Time.now,
72 :comments => '[#1 #2, #3] Worked on these')
73 c.scan_comment_for_issue_ids
74
75 assert_equal [1,2,3], c.issue_ids.sort
76 end
77
56 def test_previous
78 def test_previous
57 changeset = Changeset.find_by_revision('3')
79 changeset = Changeset.find_by_revision('3')
58 assert_equal Changeset.find_by_revision('2'), changeset.previous
80 assert_equal Changeset.find_by_revision('2'), changeset.previous
59 end
81 end
60
82
61 def test_previous_nil
83 def test_previous_nil
62 changeset = Changeset.find_by_revision('1')
84 changeset = Changeset.find_by_revision('1')
63 assert_nil changeset.previous
85 assert_nil changeset.previous
64 end
86 end
65
87
66 def test_next
88 def test_next
67 changeset = Changeset.find_by_revision('2')
89 changeset = Changeset.find_by_revision('2')
68 assert_equal Changeset.find_by_revision('3'), changeset.next
90 assert_equal Changeset.find_by_revision('3'), changeset.next
69 end
91 end
70
92
71 def test_next_nil
93 def test_next_nil
72 changeset = Changeset.find_by_revision('8')
94 changeset = Changeset.find_by_revision('8')
73 assert_nil changeset.next
95 assert_nil changeset.next
74 end
96 end
75 end
97 end
General Comments 0
You need to be logged in to leave comments. Login now