##// END OF EJS Templates
Fixes utf8 conversions with ruby1.9....
Jean-Philippe Lang -
r4485:ad727d378100
parent child
Show More
@@ -1,40 +1,46
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 module AttachmentsHelper
18 module AttachmentsHelper
19 # Displays view/delete links to the attachments of the given object
19 # Displays view/delete links to the attachments of the given object
20 # Options:
20 # Options:
21 # :author -- author names are not displayed if set to false
21 # :author -- author names are not displayed if set to false
22 def link_to_attachments(container, options = {})
22 def link_to_attachments(container, options = {})
23 options.assert_valid_keys(:author)
23 options.assert_valid_keys(:author)
24
24
25 if container.attachments.any?
25 if container.attachments.any?
26 options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
26 options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
27 render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
27 render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
28 end
28 end
29 end
29 end
30
30
31 def to_utf8(str)
31 def to_utf8(str)
32 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
32 if str.respond_to?(:force_encoding)
33 str.force_encoding('UTF-8')
34 return str if str.valid_encoding?
35 else
36 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
37 end
38
33 begin
39 begin
34 Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
40 Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
35 rescue Iconv::InvalidEncoding
41 rescue Iconv::InvalidEncoding
36 # "UTF-8//IGNORE" is not supported on some OS
42 # "UTF-8//IGNORE" is not supported on some OS
37 str
43 str
38 end
44 end
39 end
45 end
40 end
46 end
@@ -1,188 +1,194
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 module RepositoriesHelper
20 module RepositoriesHelper
21 def format_revision(txt)
21 def format_revision(txt)
22 txt.to_s[0,8]
22 txt.to_s[0,8]
23 end
23 end
24
24
25 def truncate_at_line_break(text, length = 255)
25 def truncate_at_line_break(text, length = 255)
26 if text
26 if text
27 text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
27 text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
28 end
28 end
29 end
29 end
30
30
31 def render_properties(properties)
31 def render_properties(properties)
32 unless properties.nil? || properties.empty?
32 unless properties.nil? || properties.empty?
33 content = ''
33 content = ''
34 properties.keys.sort.each do |property|
34 properties.keys.sort.each do |property|
35 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>")
35 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>")
36 end
36 end
37 content_tag('ul', content, :class => 'properties')
37 content_tag('ul', content, :class => 'properties')
38 end
38 end
39 end
39 end
40
40
41 def render_changeset_changes
41 def render_changeset_changes
42 changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change|
42 changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change|
43 case change.action
43 case change.action
44 when 'A'
44 when 'A'
45 # Detects moved/copied files
45 # Detects moved/copied files
46 if !change.from_path.blank?
46 if !change.from_path.blank?
47 change.action = @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
47 change.action = @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
48 end
48 end
49 change
49 change
50 when 'D'
50 when 'D'
51 @changeset.changes.detect {|c| c.from_path == change.path} ? nil : change
51 @changeset.changes.detect {|c| c.from_path == change.path} ? nil : change
52 else
52 else
53 change
53 change
54 end
54 end
55 end.compact
55 end.compact
56
56
57 tree = { }
57 tree = { }
58 changes.each do |change|
58 changes.each do |change|
59 p = tree
59 p = tree
60 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
60 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
61 path = ''
61 path = ''
62 dirs.each do |dir|
62 dirs.each do |dir|
63 path += '/' + dir
63 path += '/' + dir
64 p[:s] ||= {}
64 p[:s] ||= {}
65 p = p[:s]
65 p = p[:s]
66 p[path] ||= {}
66 p[path] ||= {}
67 p = p[path]
67 p = p[path]
68 end
68 end
69 p[:c] = change
69 p[:c] = change
70 end
70 end
71
71
72 render_changes_tree(tree[:s])
72 render_changes_tree(tree[:s])
73 end
73 end
74
74
75 def render_changes_tree(tree)
75 def render_changes_tree(tree)
76 return '' if tree.nil?
76 return '' if tree.nil?
77
77
78 output = ''
78 output = ''
79 output << '<ul>'
79 output << '<ul>'
80 tree.keys.sort.each do |file|
80 tree.keys.sort.each do |file|
81 style = 'change'
81 style = 'change'
82 text = File.basename(h(file))
82 text = File.basename(h(file))
83 if s = tree[file][:s]
83 if s = tree[file][:s]
84 style << ' folder'
84 style << ' folder'
85 path_param = to_path_param(@repository.relative_path(file))
85 path_param = to_path_param(@repository.relative_path(file))
86 text = link_to(text, :controller => 'repositories',
86 text = link_to(text, :controller => 'repositories',
87 :action => 'show',
87 :action => 'show',
88 :id => @project,
88 :id => @project,
89 :path => path_param,
89 :path => path_param,
90 :rev => @changeset.revision)
90 :rev => @changeset.revision)
91 output << "<li class='#{style}'>#{text}</li>"
91 output << "<li class='#{style}'>#{text}</li>"
92 output << render_changes_tree(s)
92 output << render_changes_tree(s)
93 elsif c = tree[file][:c]
93 elsif c = tree[file][:c]
94 style << " change-#{c.action}"
94 style << " change-#{c.action}"
95 path_param = to_path_param(@repository.relative_path(c.path))
95 path_param = to_path_param(@repository.relative_path(c.path))
96 text = link_to(text, :controller => 'repositories',
96 text = link_to(text, :controller => 'repositories',
97 :action => 'entry',
97 :action => 'entry',
98 :id => @project,
98 :id => @project,
99 :path => path_param,
99 :path => path_param,
100 :rev => @changeset.revision) unless c.action == 'D'
100 :rev => @changeset.revision) unless c.action == 'D'
101 text << " - #{c.revision}" unless c.revision.blank?
101 text << " - #{c.revision}" unless c.revision.blank?
102 text << ' (' + link_to('diff', :controller => 'repositories',
102 text << ' (' + link_to('diff', :controller => 'repositories',
103 :action => 'diff',
103 :action => 'diff',
104 :id => @project,
104 :id => @project,
105 :path => path_param,
105 :path => path_param,
106 :rev => @changeset.revision) + ') ' if c.action == 'M'
106 :rev => @changeset.revision) + ') ' if c.action == 'M'
107 text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
107 text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
108 output << "<li class='#{style}'>#{text}</li>"
108 output << "<li class='#{style}'>#{text}</li>"
109 end
109 end
110 end
110 end
111 output << '</ul>'
111 output << '</ul>'
112 output
112 output
113 end
113 end
114
114
115 def to_utf8(str)
115 def to_utf8(str)
116 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
116 if str.respond_to?(:force_encoding)
117 str.force_encoding('UTF-8')
118 return str if str.valid_encoding?
119 else
120 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
121 end
122
117 @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
123 @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
118 @encodings.each do |encoding|
124 @encodings.each do |encoding|
119 begin
125 begin
120 return Iconv.conv('UTF-8', encoding, str)
126 return Iconv.conv('UTF-8', encoding, str)
121 rescue Iconv::Failure
127 rescue Iconv::Failure
122 # do nothing here and try the next encoding
128 # do nothing here and try the next encoding
123 end
129 end
124 end
130 end
125 str
131 str
126 end
132 end
127
133
128 def repository_field_tags(form, repository)
134 def repository_field_tags(form, repository)
129 method = repository.class.name.demodulize.underscore + "_field_tags"
135 method = repository.class.name.demodulize.underscore + "_field_tags"
130 send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method) && method != 'repository_field_tags'
136 send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method) && method != 'repository_field_tags'
131 end
137 end
132
138
133 def scm_select_tag(repository)
139 def scm_select_tag(repository)
134 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
140 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
135 Redmine::Scm::Base.all.each do |scm|
141 Redmine::Scm::Base.all.each do |scm|
136 scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm)
142 scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm)
137 end
143 end
138
144
139 select_tag('repository_scm',
145 select_tag('repository_scm',
140 options_for_select(scm_options, repository.class.name.demodulize),
146 options_for_select(scm_options, repository.class.name.demodulize),
141 :disabled => (repository && !repository.new_record?),
147 :disabled => (repository && !repository.new_record?),
142 :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
148 :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
143 )
149 )
144 end
150 end
145
151
146 def with_leading_slash(path)
152 def with_leading_slash(path)
147 path.to_s.starts_with?('/') ? path : "/#{path}"
153 path.to_s.starts_with?('/') ? path : "/#{path}"
148 end
154 end
149
155
150 def without_leading_slash(path)
156 def without_leading_slash(path)
151 path.gsub(%r{^/+}, '')
157 path.gsub(%r{^/+}, '')
152 end
158 end
153
159
154 def subversion_field_tags(form, repository)
160 def subversion_field_tags(form, repository)
155 content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
161 content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
156 '<br />(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
162 '<br />(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
157 content_tag('p', form.text_field(:login, :size => 30)) +
163 content_tag('p', form.text_field(:login, :size => 30)) +
158 content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
164 content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
159 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
165 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
160 :onfocus => "this.value=''; this.name='repository[password]';",
166 :onfocus => "this.value=''; this.name='repository[password]';",
161 :onchange => "this.name='repository[password]';"))
167 :onchange => "this.name='repository[password]';"))
162 end
168 end
163
169
164 def darcs_field_tags(form, repository)
170 def darcs_field_tags(form, repository)
165 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
171 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
166 end
172 end
167
173
168 def mercurial_field_tags(form, repository)
174 def mercurial_field_tags(form, repository)
169 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
175 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
170 end
176 end
171
177
172 def git_field_tags(form, repository)
178 def git_field_tags(form, repository)
173 content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
179 content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
174 end
180 end
175
181
176 def cvs_field_tags(form, repository)
182 def cvs_field_tags(form, repository)
177 content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
183 content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
178 content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
184 content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
179 end
185 end
180
186
181 def bazaar_field_tags(form, repository)
187 def bazaar_field_tags(form, repository)
182 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
188 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
183 end
189 end
184
190
185 def filesystem_field_tags(form, repository)
191 def filesystem_field_tags(form, repository)
186 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
192 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
187 end
193 end
188 end
194 end
@@ -1,247 +1,253
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
44 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
45 :conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
45 :conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
46
46
47 def revision=(r)
47 def revision=(r)
48 write_attribute :revision, (r.nil? ? nil : r.to_s)
48 write_attribute :revision, (r.nil? ? nil : r.to_s)
49 end
49 end
50
50
51 def comments=(comment)
51 def comments=(comment)
52 write_attribute(:comments, Changeset.normalize_comments(comment))
52 write_attribute(:comments, Changeset.normalize_comments(comment))
53 end
53 end
54
54
55 def committed_on=(date)
55 def committed_on=(date)
56 self.commit_date = date
56 self.commit_date = date
57 super
57 super
58 end
58 end
59
59
60 def committer=(arg)
60 def committer=(arg)
61 write_attribute(:committer, self.class.to_utf8(arg.to_s))
61 write_attribute(:committer, self.class.to_utf8(arg.to_s))
62 end
62 end
63
63
64 def project
64 def project
65 repository.project
65 repository.project
66 end
66 end
67
67
68 def author
68 def author
69 user || committer.to_s.split('<').first
69 user || committer.to_s.split('<').first
70 end
70 end
71
71
72 def before_create
72 def before_create
73 self.user = repository.find_committer_user(committer)
73 self.user = repository.find_committer_user(committer)
74 end
74 end
75
75
76 def after_create
76 def after_create
77 scan_comment_for_issue_ids
77 scan_comment_for_issue_ids
78 end
78 end
79
79
80 TIMELOG_RE = /
80 TIMELOG_RE = /
81 (
81 (
82 (\d+([.,]\d+)?)h?
82 (\d+([.,]\d+)?)h?
83 |
83 |
84 (\d+):(\d+)
84 (\d+):(\d+)
85 |
85 |
86 ((\d+)(h|hours?))?((\d+)(m|min)?)?
86 ((\d+)(h|hours?))?((\d+)(m|min)?)?
87 )
87 )
88 /x
88 /x
89
89
90 def scan_comment_for_issue_ids
90 def scan_comment_for_issue_ids
91 return if comments.blank?
91 return if comments.blank?
92 # keywords used to reference issues
92 # keywords used to reference issues
93 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
93 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
94 ref_keywords_any = ref_keywords.delete('*')
94 ref_keywords_any = ref_keywords.delete('*')
95 # keywords used to fix issues
95 # keywords used to fix issues
96 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
96 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
97
97
98 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
98 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
99
99
100 referenced_issues = []
100 referenced_issues = []
101
101
102 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
102 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
103 action, refs = match[2], match[3]
103 action, refs = match[2], match[3]
104 next unless action.present? || ref_keywords_any
104 next unless action.present? || ref_keywords_any
105
105
106 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
106 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
107 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
107 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
108 if issue
108 if issue
109 referenced_issues << issue
109 referenced_issues << issue
110 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
110 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
111 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
111 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
112 end
112 end
113 end
113 end
114 end
114 end
115
115
116 referenced_issues.uniq!
116 referenced_issues.uniq!
117 self.issues = referenced_issues unless referenced_issues.empty?
117 self.issues = referenced_issues unless referenced_issues.empty?
118 end
118 end
119
119
120 def short_comments
120 def short_comments
121 @short_comments || split_comments.first
121 @short_comments || split_comments.first
122 end
122 end
123
123
124 def long_comments
124 def long_comments
125 @long_comments || split_comments.last
125 @long_comments || split_comments.last
126 end
126 end
127
127
128 def text_tag
128 def text_tag
129 if scmid?
129 if scmid?
130 "commit:#{scmid}"
130 "commit:#{scmid}"
131 else
131 else
132 "r#{revision}"
132 "r#{revision}"
133 end
133 end
134 end
134 end
135
135
136 # Returns the previous changeset
136 # Returns the previous changeset
137 def previous
137 def previous
138 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
138 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
139 end
139 end
140
140
141 # Returns the next changeset
141 # Returns the next changeset
142 def next
142 def next
143 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
143 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
144 end
144 end
145
145
146 # Strips and reencodes a commit log before insertion into the database
146 # Strips and reencodes a commit log before insertion into the database
147 def self.normalize_comments(str)
147 def self.normalize_comments(str)
148 to_utf8(str.to_s.strip)
148 to_utf8(str.to_s.strip)
149 end
149 end
150
150
151 # Creates a new Change from it's common parameters
151 # Creates a new Change from it's common parameters
152 def create_change(change)
152 def create_change(change)
153 Change.create(:changeset => self,
153 Change.create(:changeset => self,
154 :action => change[:action],
154 :action => change[:action],
155 :path => change[:path],
155 :path => change[:path],
156 :from_path => change[:from_path],
156 :from_path => change[:from_path],
157 :from_revision => change[:from_revision])
157 :from_revision => change[:from_revision])
158 end
158 end
159
159
160 private
160 private
161
161
162 # Finds an issue that can be referenced by the commit message
162 # Finds an issue that can be referenced by the commit message
163 # i.e. an issue that belong to the repository project, a subproject or a parent project
163 # i.e. an issue that belong to the repository project, a subproject or a parent project
164 def find_referenced_issue_by_id(id)
164 def find_referenced_issue_by_id(id)
165 return nil if id.blank?
165 return nil if id.blank?
166 issue = Issue.find_by_id(id.to_i, :include => :project)
166 issue = Issue.find_by_id(id.to_i, :include => :project)
167 if issue
167 if issue
168 unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
168 unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
169 issue = nil
169 issue = nil
170 end
170 end
171 end
171 end
172 issue
172 issue
173 end
173 end
174
174
175 def fix_issue(issue)
175 def fix_issue(issue)
176 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
176 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
177 if status.nil?
177 if status.nil?
178 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
178 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
179 return issue
179 return issue
180 end
180 end
181
181
182 # the issue may have been updated by the closure of another one (eg. duplicate)
182 # the issue may have been updated by the closure of another one (eg. duplicate)
183 issue.reload
183 issue.reload
184 # don't change the status is the issue is closed
184 # don't change the status is the issue is closed
185 return if issue.status && issue.status.is_closed?
185 return if issue.status && issue.status.is_closed?
186
186
187 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
187 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
188 issue.status = status
188 issue.status = status
189 unless Setting.commit_fix_done_ratio.blank?
189 unless Setting.commit_fix_done_ratio.blank?
190 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
190 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
191 end
191 end
192 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
192 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
193 { :changeset => self, :issue => issue })
193 { :changeset => self, :issue => issue })
194 unless issue.save
194 unless issue.save
195 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
195 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
196 end
196 end
197 issue
197 issue
198 end
198 end
199
199
200 def log_time(issue, hours)
200 def log_time(issue, hours)
201 time_entry = TimeEntry.new(
201 time_entry = TimeEntry.new(
202 :user => user,
202 :user => user,
203 :hours => hours,
203 :hours => hours,
204 :issue => issue,
204 :issue => issue,
205 :spent_on => commit_date,
205 :spent_on => commit_date,
206 :comments => l(:text_time_logged_by_changeset, :value => text_tag, :locale => Setting.default_language)
206 :comments => l(:text_time_logged_by_changeset, :value => text_tag, :locale => Setting.default_language)
207 )
207 )
208 time_entry.activity = log_time_activity unless log_time_activity.nil?
208 time_entry.activity = log_time_activity unless log_time_activity.nil?
209
209
210 unless time_entry.save
210 unless time_entry.save
211 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
211 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
212 end
212 end
213 time_entry
213 time_entry
214 end
214 end
215
215
216 def log_time_activity
216 def log_time_activity
217 if Setting.commit_logtime_activity_id.to_i > 0
217 if Setting.commit_logtime_activity_id.to_i > 0
218 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
218 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
219 end
219 end
220 end
220 end
221
221
222 def split_comments
222 def split_comments
223 comments =~ /\A(.+?)\r?\n(.*)$/m
223 comments =~ /\A(.+?)\r?\n(.*)$/m
224 @short_comments = $1 || comments
224 @short_comments = $1 || comments
225 @long_comments = $2.to_s.strip
225 @long_comments = $2.to_s.strip
226 return @short_comments, @long_comments
226 return @short_comments, @long_comments
227 end
227 end
228
228
229 def self.to_utf8(str)
229 def self.to_utf8(str)
230 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
230 if str.respond_to?(:force_encoding)
231 str.force_encoding('UTF-8')
232 return str if str.valid_encoding?
233 else
234 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
235 end
236
231 encoding = Setting.commit_logs_encoding.to_s.strip
237 encoding = Setting.commit_logs_encoding.to_s.strip
232 unless encoding.blank? || encoding == 'UTF-8'
238 unless encoding.blank? || encoding == 'UTF-8'
233 begin
239 begin
234 str = Iconv.conv('UTF-8', encoding, str)
240 str = Iconv.conv('UTF-8', encoding, str)
235 rescue Iconv::Failure
241 rescue Iconv::Failure
236 # do nothing here
242 # do nothing here
237 end
243 end
238 end
244 end
239 # removes invalid UTF8 sequences
245 # removes invalid UTF8 sequences
240 begin
246 begin
241 Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
247 Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
242 rescue Iconv::InvalidEncoding
248 rescue Iconv::InvalidEncoding
243 # "UTF-8//IGNORE" is not supported on some OS
249 # "UTF-8//IGNORE" is not supported on some OS
244 str
250 str
245 end
251 end
246 end
252 end
247 end
253 end
General Comments 0
You need to be logged in to leave comments. Login now