##// END OF EJS Templates
Ability to define commit keywords per tracker (#7590)....
Jean-Philippe Lang -
r11978:b6cb7aa8e3b9
parent child
Show More
@@ -1,74 +1,74
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class SettingsController < ApplicationController
19 19 layout 'admin'
20 20 menu_item :plugins, :only => :plugin
21 21
22 22 helper :queries
23 23
24 24 before_filter :require_admin
25 25
26 26 def index
27 27 edit
28 28 render :action => 'edit'
29 29 end
30 30
31 31 def edit
32 32 @notifiables = Redmine::Notifiable.all
33 33 if request.post? && params[:settings] && params[:settings].is_a?(Hash)
34 34 settings = (params[:settings] || {}).dup.symbolize_keys
35 35 settings.each do |name, value|
36 36 Setting.set_from_params name, value
37 37 end
38 38 flash[:notice] = l(:notice_successful_update)
39 39 redirect_to settings_path(:tab => params[:tab])
40 40 else
41 41 @options = {}
42 42 user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]}
43 43 @options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]}
44 44 @deliveries = ActionMailer::Base.perform_deliveries
45 45
46 46 @guessed_host_and_path = request.host_with_port.dup
47 47 @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
48 48
49 49 @commit_update_keywords = Setting.commit_update_keywords.dup
50 @commit_update_keywords[''] = {} if @commit_update_keywords.blank?
50 @commit_update_keywords << {} if @commit_update_keywords.blank?
51 51
52 52 Redmine::Themes.rescan
53 53 end
54 54 end
55 55
56 56 def plugin
57 57 @plugin = Redmine::Plugin.find(params[:id])
58 58 unless @plugin.configurable?
59 59 render_404
60 60 return
61 61 end
62 62
63 63 if request.post?
64 64 Setting.send "plugin_#{@plugin.id}=", params[:settings]
65 65 flash[:notice] = l(:notice_successful_update)
66 66 redirect_to plugin_settings_path(@plugin)
67 67 else
68 68 @partial = @plugin.settings[:partial]
69 69 @settings = Setting.send "plugin_#{@plugin.id}"
70 70 end
71 71 rescue Redmine::PluginNotFound
72 72 render_404
73 73 end
74 74 end
@@ -1,276 +1,278
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Changeset < ActiveRecord::Base
19 19 belongs_to :repository
20 20 belongs_to :user
21 21 has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
22 22 has_and_belongs_to_many :issues
23 23 has_and_belongs_to_many :parents,
24 24 :class_name => "Changeset",
25 25 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
26 26 :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
27 27 has_and_belongs_to_many :children,
28 28 :class_name => "Changeset",
29 29 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
30 30 :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
31 31
32 32 acts_as_event :title => Proc.new {|o| o.title},
33 33 :description => :long_comments,
34 34 :datetime => :committed_on,
35 35 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
36 36
37 37 acts_as_searchable :columns => 'comments',
38 38 :include => {:repository => :project},
39 39 :project_key => "#{Repository.table_name}.project_id",
40 40 :date_column => 'committed_on'
41 41
42 42 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
43 43 :author_key => :user_id,
44 44 :find_options => {:include => [:user, {:repository => :project}]}
45 45
46 46 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
47 47 validates_uniqueness_of :revision, :scope => :repository_id
48 48 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
49 49
50 50 scope :visible, lambda {|*args|
51 51 includes(:repository => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
52 52 }
53 53
54 54 after_create :scan_for_issues
55 55 before_create :before_create_cs
56 56
57 57 def revision=(r)
58 58 write_attribute :revision, (r.nil? ? nil : r.to_s)
59 59 end
60 60
61 61 # Returns the identifier of this changeset; depending on repository backends
62 62 def identifier
63 63 if repository.class.respond_to? :changeset_identifier
64 64 repository.class.changeset_identifier self
65 65 else
66 66 revision.to_s
67 67 end
68 68 end
69 69
70 70 def committed_on=(date)
71 71 self.commit_date = date
72 72 super
73 73 end
74 74
75 75 # Returns the readable identifier
76 76 def format_identifier
77 77 if repository.class.respond_to? :format_changeset_identifier
78 78 repository.class.format_changeset_identifier self
79 79 else
80 80 identifier
81 81 end
82 82 end
83 83
84 84 def project
85 85 repository.project
86 86 end
87 87
88 88 def author
89 89 user || committer.to_s.split('<').first
90 90 end
91 91
92 92 def before_create_cs
93 93 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
94 94 self.comments = self.class.normalize_comments(
95 95 self.comments, repository.repo_log_encoding)
96 96 self.user = repository.find_committer_user(self.committer)
97 97 end
98 98
99 99 def scan_for_issues
100 100 scan_comment_for_issue_ids
101 101 end
102 102
103 103 TIMELOG_RE = /
104 104 (
105 105 ((\d+)(h|hours?))((\d+)(m|min)?)?
106 106 |
107 107 ((\d+)(h|hours?|m|min))
108 108 |
109 109 (\d+):(\d+)
110 110 |
111 111 (\d+([\.,]\d+)?)h?
112 112 )
113 113 /x
114 114
115 115 def scan_comment_for_issue_ids
116 116 return if comments.blank?
117 117 # keywords used to reference issues
118 118 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
119 119 ref_keywords_any = ref_keywords.delete('*')
120 120 # keywords used to fix issues
121 fix_keywords = Setting.commit_update_by_keyword.keys
121 fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
122 122
123 123 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
124 124
125 125 referenced_issues = []
126 126
127 127 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
128 128 action, refs = match[2].to_s.downcase, match[3]
129 129 next unless action.present? || ref_keywords_any
130 130
131 131 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
132 132 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
133 133 if issue
134 134 referenced_issues << issue
135 135 # Don't update issues or log time when importing old commits
136 136 unless repository.created_on && committed_on && committed_on < repository.created_on
137 137 fix_issue(issue, action) if fix_keywords.include?(action)
138 138 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
139 139 end
140 140 end
141 141 end
142 142 end
143 143
144 144 referenced_issues.uniq!
145 145 self.issues = referenced_issues unless referenced_issues.empty?
146 146 end
147 147
148 148 def short_comments
149 149 @short_comments || split_comments.first
150 150 end
151 151
152 152 def long_comments
153 153 @long_comments || split_comments.last
154 154 end
155 155
156 156 def text_tag(ref_project=nil)
157 157 tag = if scmid?
158 158 "commit:#{scmid}"
159 159 else
160 160 "r#{revision}"
161 161 end
162 162 if repository && repository.identifier.present?
163 163 tag = "#{repository.identifier}|#{tag}"
164 164 end
165 165 if ref_project && project && ref_project != project
166 166 tag = "#{project.identifier}:#{tag}"
167 167 end
168 168 tag
169 169 end
170 170
171 171 # Returns the title used for the changeset in the activity/search results
172 172 def title
173 173 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
174 174 comm = short_comments.blank? ? '' : (': ' + short_comments)
175 175 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
176 176 end
177 177
178 178 # Returns the previous changeset
179 179 def previous
180 180 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
181 181 end
182 182
183 183 # Returns the next changeset
184 184 def next
185 185 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
186 186 end
187 187
188 188 # Creates a new Change from it's common parameters
189 189 def create_change(change)
190 190 Change.create(:changeset => self,
191 191 :action => change[:action],
192 192 :path => change[:path],
193 193 :from_path => change[:from_path],
194 194 :from_revision => change[:from_revision])
195 195 end
196 196
197 197 # Finds an issue that can be referenced by the commit message
198 198 def find_referenced_issue_by_id(id)
199 199 return nil if id.blank?
200 200 issue = Issue.find_by_id(id.to_i, :include => :project)
201 201 if Setting.commit_cross_project_ref?
202 202 # all issues can be referenced/fixed
203 203 elsif issue
204 204 # issue that belong to the repository project, a subproject or a parent project only
205 205 unless issue.project &&
206 206 (project == issue.project || project.is_ancestor_of?(issue.project) ||
207 207 project.is_descendant_of?(issue.project))
208 208 issue = nil
209 209 end
210 210 end
211 211 issue
212 212 end
213 213
214 214 private
215 215
216 216 # Updates the +issue+ according to +action+
217 217 def fix_issue(issue, action)
218 updates = Setting.commit_update_by_keyword[action]
219 return unless updates.is_a?(Hash)
220
221 218 # the issue may have been updated by the closure of another one (eg. duplicate)
222 219 issue.reload
223 220 # don't change the status is the issue is closed
224 221 return if issue.status && issue.status.is_closed?
225 222
226 223 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
227 issue.assign_attributes updates.slice(*Issue.attribute_names)
224 rule = Setting.commit_update_keywords_array.detect do |rule|
225 rule['keywords'].include?(action) && (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
226 end
227 if rule
228 issue.assign_attributes rule.slice(*Issue.attribute_names)
229 end
228 230 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
229 231 { :changeset => self, :issue => issue, :action => action })
230 232 unless issue.save
231 233 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
232 234 end
233 235 issue
234 236 end
235 237
236 238 def log_time(issue, hours)
237 239 time_entry = TimeEntry.new(
238 240 :user => user,
239 241 :hours => hours,
240 242 :issue => issue,
241 243 :spent_on => commit_date,
242 244 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
243 245 :locale => Setting.default_language)
244 246 )
245 247 time_entry.activity = log_time_activity unless log_time_activity.nil?
246 248
247 249 unless time_entry.save
248 250 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
249 251 end
250 252 time_entry
251 253 end
252 254
253 255 def log_time_activity
254 256 if Setting.commit_logtime_activity_id.to_i > 0
255 257 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
256 258 end
257 259 end
258 260
259 261 def split_comments
260 262 comments =~ /\A(.+?)\r?\n(.*)$/m
261 263 @short_comments = $1 || comments
262 264 @long_comments = $2.to_s.strip
263 265 return @short_comments, @long_comments
264 266 end
265 267
266 268 public
267 269
268 270 # Strips and reencodes a commit log before insertion into the database
269 271 def self.normalize_comments(str, encoding)
270 272 Changeset.to_utf8(str.to_s.strip, encoding)
271 273 end
272 274
273 275 def self.to_utf8(str, encoding)
274 276 Redmine::CodesetUtil.to_utf8(str, encoding)
275 277 end
276 278 end
@@ -1,250 +1,250
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Setting < ActiveRecord::Base
19 19
20 20 DATE_FORMATS = [
21 21 '%Y-%m-%d',
22 22 '%d/%m/%Y',
23 23 '%d.%m.%Y',
24 24 '%d-%m-%Y',
25 25 '%m/%d/%Y',
26 26 '%d %b %Y',
27 27 '%d %B %Y',
28 28 '%b %d, %Y',
29 29 '%B %d, %Y'
30 30 ]
31 31
32 32 TIME_FORMATS = [
33 33 '%H:%M',
34 34 '%I:%M %p'
35 35 ]
36 36
37 37 ENCODINGS = %w(US-ASCII
38 38 windows-1250
39 39 windows-1251
40 40 windows-1252
41 41 windows-1253
42 42 windows-1254
43 43 windows-1255
44 44 windows-1256
45 45 windows-1257
46 46 windows-1258
47 47 windows-31j
48 48 ISO-2022-JP
49 49 ISO-2022-KR
50 50 ISO-8859-1
51 51 ISO-8859-2
52 52 ISO-8859-3
53 53 ISO-8859-4
54 54 ISO-8859-5
55 55 ISO-8859-6
56 56 ISO-8859-7
57 57 ISO-8859-8
58 58 ISO-8859-9
59 59 ISO-8859-13
60 60 ISO-8859-15
61 61 KOI8-R
62 62 UTF-8
63 63 UTF-16
64 64 UTF-16BE
65 65 UTF-16LE
66 66 EUC-JP
67 67 Shift_JIS
68 68 CP932
69 69 GB18030
70 70 GBK
71 71 ISCII91
72 72 EUC-KR
73 73 Big5
74 74 Big5-HKSCS
75 75 TIS-620)
76 76
77 77 cattr_accessor :available_settings
78 78 @@available_settings = YAML::load(File.open("#{Rails.root}/config/settings.yml"))
79 79 Redmine::Plugin.all.each do |plugin|
80 80 next unless plugin.settings
81 81 @@available_settings["plugin_#{plugin.id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
82 82 end
83 83
84 84 validates_uniqueness_of :name
85 85 validates_inclusion_of :name, :in => @@available_settings.keys
86 86 validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting| @@available_settings[setting.name]['format'] == 'int' }
87 87
88 88 # Hash used to cache setting values
89 89 @cached_settings = {}
90 90 @cached_cleared_on = Time.now
91 91
92 92 def value
93 93 v = read_attribute(:value)
94 94 # Unserialize serialized settings
95 95 v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String)
96 96 v = v.to_sym if @@available_settings[name]['format'] == 'symbol' && !v.blank?
97 97 v
98 98 end
99 99
100 100 def value=(v)
101 101 v = v.to_yaml if v && @@available_settings[name] && @@available_settings[name]['serialized']
102 102 write_attribute(:value, v.to_s)
103 103 end
104 104
105 105 # Returns the value of the setting named name
106 106 def self.[](name)
107 107 v = @cached_settings[name]
108 108 v ? v : (@cached_settings[name] = find_or_default(name).value)
109 109 end
110 110
111 111 def self.[]=(name, v)
112 112 setting = find_or_default(name)
113 113 setting.value = (v ? v : "")
114 114 @cached_settings[name] = nil
115 115 setting.save
116 116 setting.value
117 117 end
118 118
119 119 # Defines getter and setter for each setting
120 120 # Then setting values can be read using: Setting.some_setting_name
121 121 # or set using Setting.some_setting_name = "some value"
122 122 @@available_settings.each do |name, params|
123 123 src = <<-END_SRC
124 124 def self.#{name}
125 125 self[:#{name}]
126 126 end
127 127
128 128 def self.#{name}?
129 129 self[:#{name}].to_i > 0
130 130 end
131 131
132 132 def self.#{name}=(value)
133 133 self[:#{name}] = value
134 134 end
135 135 END_SRC
136 136 class_eval src, __FILE__, __LINE__
137 137 end
138 138
139 139 # Sets a setting value from params
140 140 def self.set_from_params(name, params)
141 141 params = params.dup
142 142 params.delete_if {|v| v.blank? } if params.is_a?(Array)
143 143
144 144 m = "#{name}_from_params"
145 145 if respond_to? m
146 146 self[name.to_sym] = send m, params
147 147 else
148 148 self[name.to_sym] = params
149 149 end
150 150 end
151 151
152 152 # Returns a hash suitable for commit_update_keywords setting
153 153 #
154 154 # Example:
155 155 # params = {:keywords => ['fixes', 'closes'], :status_id => ["3", "5"], :done_ratio => ["", "100"]}
156 156 # Setting.commit_update_keywords_from_params(params)
157 # # => {'fixes' => {'status_id' => "3"}, 'closes' => {'status_id' => "5", 'done_ratio' => "100"}}
157 # # => [{'keywords => 'fixes', 'status_id' => "3"}, {'keywords => 'closes', 'status_id' => "5", 'done_ratio' => "100"}]
158 158 def self.commit_update_keywords_from_params(params)
159 s = {}
159 s = []
160 160 if params.is_a?(Hash) && params.key?(:keywords) && params.values.all? {|v| v.is_a? Array}
161 161 attributes = params.except(:keywords).keys
162 162 params[:keywords].each_with_index do |keywords, i|
163 163 next if keywords.blank?
164 s[keywords] = attributes.inject({}) {|h, a|
164 s << attributes.inject({}) {|h, a|
165 165 value = params[a][i].to_s
166 166 h[a.to_s] = value if value.present?
167 167 h
168 }
168 }.merge('keywords' => keywords)
169 169 end
170 170 end
171 171 s
172 172 end
173 173
174 174 # Helper that returns an array based on per_page_options setting
175 175 def self.per_page_options_array
176 176 per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
177 177 end
178 178
179 179 # Helper that returns a Hash with single update keywords as keys
180 def self.commit_update_by_keyword
181 h = {}
182 if commit_update_keywords.is_a?(Hash)
183 commit_update_keywords.each do |keywords, attribute_updates|
184 next unless attribute_updates.is_a?(Hash)
185 attribute_updates = attribute_updates.dup
186 attribute_updates.delete_if {|k, v| v.blank?}
187 keywords.to_s.split(",").map(&:strip).reject(&:blank?).each do |keyword|
188 h[keyword.downcase] = attribute_updates
180 def self.commit_update_keywords_array
181 a = []
182 if commit_update_keywords.is_a?(Array)
183 commit_update_keywords.each do |rule|
184 next unless rule.is_a?(Hash)
185 rule = rule.dup
186 rule.delete_if {|k, v| v.blank?}
187 keywords = rule['keywords'].to_s.downcase.split(",").map(&:strip).reject(&:blank?)
188 next if keywords.empty?
189 a << rule.merge('keywords' => keywords)
189 190 end
190 191 end
191 end
192 h
192 a
193 193 end
194 194
195 195 def self.commit_fix_keywords
196 196 ActiveSupport::Deprecation.warn "Setting.commit_fix_keywords is deprecated and will be removed in Redmine 3"
197 if commit_update_keywords.is_a?(Hash)
198 commit_update_keywords.keys.first
197 if commit_update_keywords.is_a?(Array)
198 commit_update_keywords.first && commit_update_keywords.first['keywords']
199 199 end
200 200 end
201 201
202 202 def self.commit_fix_status_id
203 203 ActiveSupport::Deprecation.warn "Setting.commit_fix_status_id is deprecated and will be removed in Redmine 3"
204 if commit_update_keywords.is_a?(Hash)
205 commit_update_keywords[commit_fix_keywords]['status_id']
204 if commit_update_keywords.is_a?(Array)
205 commit_update_keywords.first && commit_update_keywords.first['status_id']
206 206 end
207 207 end
208 208
209 209 def self.commit_fix_done_ratio
210 210 ActiveSupport::Deprecation.warn "Setting.commit_fix_done_ratio is deprecated and will be removed in Redmine 3"
211 if commit_update_keywords.is_a?(Hash)
212 commit_update_keywords[commit_fix_keywords]['done_ratio']
211 if commit_update_keywords.is_a?(Array)
212 commit_update_keywords.first && commit_update_keywords.first['done_ratio']
213 213 end
214 214 end
215 215
216 216 def self.openid?
217 217 Object.const_defined?(:OpenID) && self[:openid].to_i > 0
218 218 end
219 219
220 220 # Checks if settings have changed since the values were read
221 221 # and clears the cache hash if it's the case
222 222 # Called once per request
223 223 def self.check_cache
224 224 settings_updated_on = Setting.maximum(:updated_on)
225 225 if settings_updated_on && @cached_cleared_on <= settings_updated_on
226 226 clear_cache
227 227 end
228 228 end
229 229
230 230 # Clears the settings cache
231 231 def self.clear_cache
232 232 @cached_settings.clear
233 233 @cached_cleared_on = Time.now
234 234 logger.info "Settings cache cleared." if logger
235 235 end
236 236
237 237 private
238 238 # Returns the Setting instance for the setting named name
239 239 # (record found in database or new record with default value)
240 240 def self.find_or_default(name)
241 241 name = name.to_s
242 242 raise "There's no setting named #{name}" unless @@available_settings.has_key?(name)
243 243 setting = find_by_name(name)
244 244 unless setting
245 245 setting = new(:name => name)
246 246 setting.value = @@available_settings[name]['default']
247 247 end
248 248 setting
249 249 end
250 250 end
@@ -1,124 +1,127
1 1 <%= form_tag({:action => 'edit', :tab => 'repositories'}) do %>
2 2
3 3 <fieldset class="box settings enabled_scm">
4 4 <legend><%= l(:setting_enabled_scm) %></legend>
5 5 <%= hidden_field_tag 'settings[enabled_scm][]', '' %>
6 6 <table>
7 7 <tr>
8 8 <th></th>
9 9 <th><%= l(:text_scm_command) %></th>
10 10 <th><%= l(:text_scm_command_version) %></th>
11 11 </tr>
12 12 <% Redmine::Scm::Base.all.collect do |choice| %>
13 13 <% scm_class = "Repository::#{choice}".constantize %>
14 14 <% text, value = (choice.is_a?(Array) ? choice : [choice, choice]) %>
15 15 <% setting = :enabled_scm %>
16 16 <% enabled = Setting.send(setting).include?(value) %>
17 17 <tr>
18 18 <td class="scm_name">
19 19 <label>
20 20 <%= check_box_tag("settings[#{setting}][]", value, enabled, :id => nil) %>
21 21 <%= text.to_s %>
22 22 </label>
23 23 </td>
24 24 <td>
25 25 <% if enabled %>
26 26 <%=
27 27 image_tag(
28 28 (scm_class.scm_available ? 'true.png' : 'exclamation.png'),
29 29 :style => "vertical-align:bottom;"
30 30 )
31 31 %>
32 32 <%= scm_class.scm_command %>
33 33 <% end %>
34 34 </td>
35 35 <td>
36 36 <%= scm_class.scm_version_string if enabled %>
37 37 </td>
38 38 </tr>
39 39 <% end %>
40 40 </table>
41 41 <p><em class="info"><%= l(:text_scm_config) %></em></p>
42 42 </fieldset>
43 43
44 44 <div class="box tabular settings">
45 45 <p><%= setting_check_box :autofetch_changesets %></p>
46 46
47 47 <p><%= setting_check_box :sys_api_enabled,
48 48 :onclick =>
49 49 "if (this.checked) { $('#settings_sys_api_key').removeAttr('disabled'); } else { $('#settings_sys_api_key').attr('disabled', true); }" %></p>
50 50
51 51 <p><%= setting_text_field :sys_api_key,
52 52 :size => 30,
53 53 :id => 'settings_sys_api_key',
54 54 :disabled => !Setting.sys_api_enabled?,
55 55 :label => :setting_mail_handler_api_key %>
56 56 <%= link_to_function l(:label_generate_key),
57 57 "if (!$('#settings_sys_api_key').attr('disabled')) { $('#settings_sys_api_key').val(randomKey(20)) }" %>
58 58 </p>
59 59
60 60 <p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
61 61 </div>
62 62
63 63 <fieldset class="box tabular settings">
64 64 <legend><%= l(:text_issues_ref_in_commit_messages) %></legend>
65 65 <p><%= setting_text_field :commit_ref_keywords, :size => 30 %>
66 66 <em class="info"><%= l(:text_comma_separated) %></em></p>
67 67
68 68 <p><%= setting_check_box :commit_cross_project_ref %></p>
69 69
70 70 <p><%= setting_check_box :commit_logtime_enabled,
71 71 :onclick =>
72 72 "if (this.checked) { $('#settings_commit_logtime_activity_id').removeAttr('disabled'); } else { $('#settings_commit_logtime_activity_id').attr('disabled', true); }"%></p>
73 73
74 74 <p><%= setting_select :commit_logtime_activity_id,
75 75 [[l(:label_default), 0]] +
76 76 TimeEntryActivity.shared.active.collect{|activity| [activity.name, activity.id.to_s]},
77 77 :disabled => !Setting.commit_logtime_enabled?%></p>
78 78 </fieldset>
79 79
80 80 <table class="list" id="commit-keywords">
81 81 <thead>
82 82 <tr>
83 <th><%= l(:label_tracker) %></th>
83 84 <th><%= l(:setting_commit_fix_keywords) %></th>
84 85 <th><%= l(:label_applied_status) %></th>
85 86 <th><%= l(:field_done_ratio) %></th>
86 87 <th class="buttons"></th>
87 88 </tr>
88 89 </thead>
89 90 <tbody>
90 <% @commit_update_keywords.each do |keywords, updates| %>
91 <% @commit_update_keywords.each do |rule| %>
91 92 <tr class="commit-keywords">
92 <td><%= text_field_tag "settings[commit_update_keywords][keywords][]", keywords, :size => 30 %></td>
93 <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>
94 <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>
93 <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>
94 <td><%= text_field_tag "settings[commit_update_keywords][keywords][]", rule['keywords'], :size => 30 %></td>
95 <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>
96 <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>
95 97 <td class="buttons"><%= link_to image_tag('delete.png'), '#', :class => 'delete-commit-keywords' %></td>
96 98 </tr>
97 99 <% end %>
98 100 <tr>
101 <td></td>
99 102 <td><em class="info"><%= l(:text_comma_separated) %></em></td>
100 103 <td></td>
101 104 <td></td>
102 105 <td class="buttons"><%= link_to image_tag('add.png'), '#', :class => 'add-commit-keywords' %></td>
103 106 </tr>
104 107 </tbody>
105 108 </table>
106 109
107 110 <p><%= submit_tag l(:button_save) %></p>
108 111 <% end %>
109 112
110 113 <%= javascript_tag do %>
111 114 $('#commit-keywords').on('click', 'a.delete-commit-keywords', function(e){
112 115 e.preventDefault();
113 116 if ($('#commit-keywords tbody tr.commit-keywords').length > 1) {
114 117 $(this).parents('#commit-keywords tr').remove();
115 118 } else {
116 119 $('#commit-keywords tbody tr.commit-keywords').find('input, select').val('');
117 120 }
118 121 });
119 122 $('#commit-keywords').on('click', 'a.add-commit-keywords', function(e){
120 123 e.preventDefault();
121 124 var row = $('#commit-keywords tr.commit-keywords:last');
122 125 row.clone().insertAfter(row).find('input, select').val('');
123 126 });
124 127 <% end %>
@@ -1,230 +1,230
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18
19 19 # DO NOT MODIFY THIS FILE !!!
20 20 # Settings can be defined through the application in Admin -> Settings
21 21
22 22 app_title:
23 23 default: Redmine
24 24 app_subtitle:
25 25 default: Project management
26 26 welcome_text:
27 27 default:
28 28 login_required:
29 29 default: 0
30 30 self_registration:
31 31 default: '2'
32 32 lost_password:
33 33 default: 1
34 34 unsubscribe:
35 35 default: 1
36 36 password_min_length:
37 37 format: int
38 38 default: 8
39 39 # Maximum lifetime of user sessions in minutes
40 40 session_lifetime:
41 41 format: int
42 42 default: 0
43 43 # User session timeout in minutes
44 44 session_timeout:
45 45 format: int
46 46 default: 0
47 47 attachment_max_size:
48 48 format: int
49 49 default: 5120
50 50 issues_export_limit:
51 51 format: int
52 52 default: 500
53 53 activity_days_default:
54 54 format: int
55 55 default: 30
56 56 per_page_options:
57 57 default: '25,50,100'
58 58 mail_from:
59 59 default: redmine@example.net
60 60 bcc_recipients:
61 61 default: 1
62 62 plain_text_mail:
63 63 default: 0
64 64 text_formatting:
65 65 default: textile
66 66 cache_formatted_text:
67 67 default: 0
68 68 wiki_compression:
69 69 default: ""
70 70 default_language:
71 71 default: en
72 72 host_name:
73 73 default: localhost:3000
74 74 protocol:
75 75 default: http
76 76 feeds_limit:
77 77 format: int
78 78 default: 15
79 79 gantt_items_limit:
80 80 format: int
81 81 default: 500
82 82 # Maximum size of files that can be displayed
83 83 # inline through the file viewer (in KB)
84 84 file_max_size_displayed:
85 85 format: int
86 86 default: 512
87 87 diff_max_lines_displayed:
88 88 format: int
89 89 default: 1500
90 90 enabled_scm:
91 91 serialized: true
92 92 default:
93 93 - Subversion
94 94 - Darcs
95 95 - Mercurial
96 96 - Cvs
97 97 - Bazaar
98 98 - Git
99 99 autofetch_changesets:
100 100 default: 1
101 101 sys_api_enabled:
102 102 default: 0
103 103 sys_api_key:
104 104 default: ''
105 105 commit_cross_project_ref:
106 106 default: 0
107 107 commit_ref_keywords:
108 108 default: 'refs,references,IssueID'
109 109 commit_update_keywords:
110 110 serialized: true
111 default: {}
111 default: []
112 112 commit_logtime_enabled:
113 113 default: 0
114 114 commit_logtime_activity_id:
115 115 format: int
116 116 default: 0
117 117 # autologin duration in days
118 118 # 0 means autologin is disabled
119 119 autologin:
120 120 format: int
121 121 default: 0
122 122 # date format
123 123 date_format:
124 124 default: ''
125 125 time_format:
126 126 default: ''
127 127 user_format:
128 128 default: :firstname_lastname
129 129 format: symbol
130 130 cross_project_issue_relations:
131 131 default: 0
132 132 # Enables subtasks to be in other projects
133 133 cross_project_subtasks:
134 134 default: 'tree'
135 135 issue_group_assignment:
136 136 default: 0
137 137 default_issue_start_date_to_creation_date:
138 138 default: 1
139 139 notified_events:
140 140 serialized: true
141 141 default:
142 142 - issue_added
143 143 - issue_updated
144 144 mail_handler_body_delimiters:
145 145 default: ''
146 146 mail_handler_excluded_filenames:
147 147 default: ''
148 148 mail_handler_api_enabled:
149 149 default: 0
150 150 mail_handler_api_key:
151 151 default:
152 152 issue_list_default_columns:
153 153 serialized: true
154 154 default:
155 155 - tracker
156 156 - status
157 157 - priority
158 158 - subject
159 159 - assigned_to
160 160 - updated_on
161 161 display_subprojects_issues:
162 162 default: 1
163 163 issue_done_ratio:
164 164 default: 'issue_field'
165 165 default_projects_public:
166 166 default: 1
167 167 default_projects_modules:
168 168 serialized: true
169 169 default:
170 170 - issue_tracking
171 171 - time_tracking
172 172 - news
173 173 - documents
174 174 - files
175 175 - wiki
176 176 - repository
177 177 - boards
178 178 - calendar
179 179 - gantt
180 180 default_projects_tracker_ids:
181 181 serialized: true
182 182 default:
183 183 # Role given to a non-admin user who creates a project
184 184 new_project_user_role_id:
185 185 format: int
186 186 default: ''
187 187 sequential_project_identifiers:
188 188 default: 0
189 189 # encodings used to convert repository files content to UTF-8
190 190 # multiple values accepted, comma separated
191 191 repositories_encodings:
192 192 default: ''
193 193 # encoding used to convert commit logs to UTF-8
194 194 commit_logs_encoding:
195 195 default: 'UTF-8'
196 196 repository_log_display_limit:
197 197 format: int
198 198 default: 100
199 199 ui_theme:
200 200 default: ''
201 201 emails_footer:
202 202 default: |-
203 203 You have received this notification because you have either subscribed to it, or are involved in it.
204 204 To change your notification preferences, please click here: http://hostname/my/account
205 205 gravatar_enabled:
206 206 default: 0
207 207 openid:
208 208 default: 0
209 209 gravatar_default:
210 210 default: ''
211 211 start_of_week:
212 212 default: ''
213 213 rest_api_enabled:
214 214 default: 0
215 215 jsonp_enabled:
216 216 default: 0
217 217 default_notification_option:
218 218 default: 'only_my_events'
219 219 emails_header:
220 220 default: ''
221 221 thumbnails_enabled:
222 222 default: 0
223 223 thumbnails_size:
224 224 format: int
225 225 default: 100
226 226 non_working_week_days:
227 227 serialized: true
228 228 default:
229 229 - '6'
230 230 - '7'
@@ -1,182 +1,186
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class SettingsControllerTest < ActionController::TestCase
21 21 fixtures :users
22 22
23 23 def setup
24 24 User.current = nil
25 25 @request.session[:user_id] = 1 # admin
26 26 end
27 27
28 28 def test_index
29 29 get :index
30 30 assert_response :success
31 31 assert_template 'edit'
32 32 end
33 33
34 34 def test_get_edit
35 35 get :edit
36 36 assert_response :success
37 37 assert_template 'edit'
38 38
39 39 assert_tag 'input', :attributes => {:name => 'settings[enabled_scm][]', :value => ''}
40 40 end
41 41
42 42 def test_get_edit_should_preselect_default_issue_list_columns
43 43 with_settings :issue_list_default_columns => %w(tracker subject status updated_on) do
44 44 get :edit
45 45 assert_response :success
46 46 end
47 47
48 48 assert_select 'select[id=selected_columns][name=?]', 'settings[issue_list_default_columns][]' do
49 49 assert_select 'option', 4
50 50 assert_select 'option[value=tracker]', :text => 'Tracker'
51 51 assert_select 'option[value=subject]', :text => 'Subject'
52 52 assert_select 'option[value=status]', :text => 'Status'
53 53 assert_select 'option[value=updated_on]', :text => 'Updated'
54 54 end
55 55
56 56 assert_select 'select[id=available_columns]' do
57 57 assert_select 'option[value=tracker]', 0
58 58 assert_select 'option[value=priority]', :text => 'Priority'
59 59 end
60 60 end
61 61
62 62 def test_get_edit_without_trackers_should_succeed
63 63 Tracker.delete_all
64 64
65 65 get :edit
66 66 assert_response :success
67 67 end
68 68
69 69 def test_post_edit_notifications
70 70 post :edit, :settings => {:mail_from => 'functional@test.foo',
71 71 :bcc_recipients => '0',
72 72 :notified_events => %w(issue_added issue_updated news_added),
73 73 :emails_footer => 'Test footer'
74 74 }
75 75 assert_redirected_to '/settings'
76 76 assert_equal 'functional@test.foo', Setting.mail_from
77 77 assert !Setting.bcc_recipients?
78 78 assert_equal %w(issue_added issue_updated news_added), Setting.notified_events
79 79 assert_equal 'Test footer', Setting.emails_footer
80 80 Setting.clear_cache
81 81 end
82 82
83 83 def test_edit_commit_update_keywords
84 with_settings :commit_update_keywords => {
85 "fixes, resolves" => {"status_id" => "3"},
86 "closes" => {"status_id" => "5", "done_ratio" => "100"}
87 } do
84 with_settings :commit_update_keywords => [
85 {"keywords" => "fixes, resolves", "status_id" => "3"},
86 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
87 ] do
88 88 get :edit
89 89 end
90 90 assert_response :success
91 91 assert_select 'tr.commit-keywords', 2
92 assert_select 'tr.commit-keywords' do
92 assert_select 'tr.commit-keywords:nth-child(1)' do
93 93 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'fixes, resolves'
94 94 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
95 95 assert_select 'option[value=3][selected=selected]'
96 96 end
97 97 end
98 assert_select 'tr.commit-keywords' do
98 assert_select 'tr.commit-keywords:nth-child(2)' do
99 99 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', 'closes'
100 100 assert_select 'select[name=?]', 'settings[commit_update_keywords][status_id][]' do
101 assert_select 'option[value=5][selected=selected]'
101 assert_select 'option[value=5][selected=selected]', :text => 'Closed'
102 102 end
103 103 assert_select 'select[name=?]', 'settings[commit_update_keywords][done_ratio][]' do
104 assert_select 'option[value=100][selected=selected]'
104 assert_select 'option[value=100][selected=selected]', :text => '100 %'
105 end
106 assert_select 'select[name=?]', 'settings[commit_update_keywords][if_tracker_id][]' do
107 assert_select 'option[value=2][selected=selected]', :text => 'Feature request'
105 108 end
106 109 end
107 110 end
108 111
109 112 def test_edit_without_commit_update_keywords_should_show_blank_line
110 with_settings :commit_update_keywords => {} do
113 with_settings :commit_update_keywords => [] do
111 114 get :edit
112 115 end
113 116 assert_response :success
114 117 assert_select 'tr.commit-keywords', 1 do
115 assert_select 'input[name=?][value=?]', 'settings[commit_update_keywords][keywords][]', ''
118 assert_select 'input[name=?]:not([value])', 'settings[commit_update_keywords][keywords][]'
116 119 end
117 120 end
118 121
119 122 def test_post_edit_commit_update_keywords
120 123 post :edit, :settings => {
121 124 :commit_update_keywords => {
122 125 :keywords => ["resolves", "closes"],
123 126 :status_id => ["3", "5"],
124 :done_ratio => ["", "100"]
127 :done_ratio => ["", "100"],
128 :if_tracker_id => ["", "2"]
125 129 }
126 130 }
127 131 assert_redirected_to '/settings'
128 assert_equal({
129 "resolves" => {"status_id" => "3"},
130 "closes" => {"status_id" => "5", "done_ratio" => "100"}
131 }, Setting.commit_update_keywords)
132 assert_equal([
133 {"keywords" => "resolves", "status_id" => "3"},
134 {"keywords" => "closes", "status_id" => "5", "done_ratio" => "100", "if_tracker_id" => "2"}
135 ], Setting.commit_update_keywords)
132 136 end
133 137
134 138 def test_get_plugin_settings
135 139 Setting.stubs(:plugin_foo).returns({'sample_setting' => 'Plugin setting value'})
136 140 ActionController::Base.append_view_path(File.join(Rails.root, "test/fixtures/plugins"))
137 141 Redmine::Plugin.register :foo do
138 142 settings :partial => "foo_plugin/foo_plugin_settings"
139 143 end
140 144
141 145 get :plugin, :id => 'foo'
142 146 assert_response :success
143 147 assert_template 'plugin'
144 148 assert_tag 'form', :attributes => {:action => '/settings/plugin/foo'},
145 149 :descendant => {:tag => 'input', :attributes => {:name => 'settings[sample_setting]', :value => 'Plugin setting value'}}
146 150
147 151 Redmine::Plugin.clear
148 152 end
149 153
150 154 def test_get_invalid_plugin_settings
151 155 get :plugin, :id => 'none'
152 156 assert_response 404
153 157 end
154 158
155 159 def test_get_non_configurable_plugin_settings
156 160 Redmine::Plugin.register(:foo) {}
157 161
158 162 get :plugin, :id => 'foo'
159 163 assert_response 404
160 164
161 165 Redmine::Plugin.clear
162 166 end
163 167
164 168 def test_post_plugin_settings
165 169 Setting.expects(:plugin_foo=).with({'sample_setting' => 'Value'}).returns(true)
166 170 Redmine::Plugin.register(:foo) do
167 171 settings :partial => 'not blank' # so that configurable? is true
168 172 end
169 173
170 174 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
171 175 assert_redirected_to '/settings/plugin/foo'
172 176 end
173 177
174 178 def test_post_non_configurable_plugin_settings
175 179 Redmine::Plugin.register(:foo) {}
176 180
177 181 post :plugin, :id => 'foo', :settings => {'sample_setting' => 'Value'}
178 182 assert_response 404
179 183
180 184 Redmine::Plugin.clear
181 185 end
182 186 end
@@ -1,169 +1,181
1 1 module ObjectHelpers
2 2 def User.generate!(attributes={})
3 3 @generated_user_login ||= 'user0'
4 4 @generated_user_login.succ!
5 5 user = User.new(attributes)
6 6 user.login = @generated_user_login.dup if user.login.blank?
7 7 user.mail = "#{@generated_user_login}@example.com" if user.mail.blank?
8 8 user.firstname = "Bob" if user.firstname.blank?
9 9 user.lastname = "Doe" if user.lastname.blank?
10 10 yield user if block_given?
11 11 user.save!
12 12 user
13 13 end
14 14
15 15 def User.add_to_project(user, project, roles=nil)
16 16 roles = Role.find(1) if roles.nil?
17 17 roles = [roles] unless roles.is_a?(Array)
18 18 Member.create!(:principal => user, :project => project, :roles => roles)
19 19 end
20 20
21 21 def Group.generate!(attributes={})
22 22 @generated_group_name ||= 'Group 0'
23 23 @generated_group_name.succ!
24 24 group = Group.new(attributes)
25 25 group.name = @generated_group_name.dup if group.name.blank?
26 26 yield group if block_given?
27 27 group.save!
28 28 group
29 29 end
30 30
31 31 def Project.generate!(attributes={})
32 32 @generated_project_identifier ||= 'project-0000'
33 33 @generated_project_identifier.succ!
34 34 project = Project.new(attributes)
35 35 project.name = @generated_project_identifier.dup if project.name.blank?
36 36 project.identifier = @generated_project_identifier.dup if project.identifier.blank?
37 37 yield project if block_given?
38 38 project.save!
39 39 project
40 40 end
41 41
42 42 def Project.generate_with_parent!(parent, attributes={})
43 43 project = Project.generate!(attributes)
44 44 project.set_parent!(parent)
45 45 project
46 46 end
47 47
48 48 def Tracker.generate!(attributes={})
49 49 @generated_tracker_name ||= 'Tracker 0'
50 50 @generated_tracker_name.succ!
51 51 tracker = Tracker.new(attributes)
52 52 tracker.name = @generated_tracker_name.dup if tracker.name.blank?
53 53 yield tracker if block_given?
54 54 tracker.save!
55 55 tracker
56 56 end
57 57
58 58 def Role.generate!(attributes={})
59 59 @generated_role_name ||= 'Role 0'
60 60 @generated_role_name.succ!
61 61 role = Role.new(attributes)
62 62 role.name = @generated_role_name.dup if role.name.blank?
63 63 yield role if block_given?
64 64 role.save!
65 65 role
66 66 end
67 67
68 68 # Generates an unsaved Issue
69 69 def Issue.generate(attributes={})
70 70 issue = Issue.new(attributes)
71 71 issue.project ||= Project.find(1)
72 72 issue.tracker ||= issue.project.trackers.first
73 73 issue.subject = 'Generated' if issue.subject.blank?
74 74 issue.author ||= User.find(2)
75 75 yield issue if block_given?
76 76 issue
77 77 end
78 78
79 79 # Generates a saved Issue
80 80 def Issue.generate!(attributes={}, &block)
81 81 issue = Issue.generate(attributes, &block)
82 82 issue.save!
83 83 issue
84 84 end
85 85
86 86 # Generates an issue with 2 children and a grandchild
87 87 def Issue.generate_with_descendants!(attributes={})
88 88 issue = Issue.generate!(attributes)
89 89 child = Issue.generate!(:project => issue.project, :subject => 'Child1', :parent_issue_id => issue.id)
90 90 Issue.generate!(:project => issue.project, :subject => 'Child2', :parent_issue_id => issue.id)
91 91 Issue.generate!(:project => issue.project, :subject => 'Child11', :parent_issue_id => child.id)
92 92 issue.reload
93 93 end
94 94
95 95 def Journal.generate!(attributes={})
96 96 journal = Journal.new(attributes)
97 97 journal.user ||= User.first
98 98 journal.journalized ||= Issue.first
99 99 yield journal if block_given?
100 100 journal.save!
101 101 journal
102 102 end
103 103
104 104 def Version.generate!(attributes={})
105 105 @generated_version_name ||= 'Version 0'
106 106 @generated_version_name.succ!
107 107 version = Version.new(attributes)
108 108 version.name = @generated_version_name.dup if version.name.blank?
109 109 yield version if block_given?
110 110 version.save!
111 111 version
112 112 end
113 113
114 114 def TimeEntry.generate!(attributes={})
115 115 entry = TimeEntry.new(attributes)
116 116 entry.user ||= User.find(2)
117 117 entry.issue ||= Issue.find(1) unless entry.project
118 118 entry.project ||= entry.issue.project
119 119 entry.activity ||= TimeEntryActivity.first
120 120 entry.spent_on ||= Date.today
121 121 entry.hours ||= 1.0
122 122 entry.save!
123 123 entry
124 124 end
125 125
126 126 def AuthSource.generate!(attributes={})
127 127 @generated_auth_source_name ||= 'Auth 0'
128 128 @generated_auth_source_name.succ!
129 129 source = AuthSource.new(attributes)
130 130 source.name = @generated_auth_source_name.dup if source.name.blank?
131 131 yield source if block_given?
132 132 source.save!
133 133 source
134 134 end
135 135
136 136 def Board.generate!(attributes={})
137 137 @generated_board_name ||= 'Forum 0'
138 138 @generated_board_name.succ!
139 139 board = Board.new(attributes)
140 140 board.name = @generated_board_name.dup if board.name.blank?
141 141 board.description = @generated_board_name.dup if board.description.blank?
142 142 yield board if block_given?
143 143 board.save!
144 144 board
145 145 end
146 146
147 147 def Attachment.generate!(attributes={})
148 148 @generated_filename ||= 'testfile0'
149 149 @generated_filename.succ!
150 150 attributes = attributes.dup
151 151 attachment = Attachment.new(attributes)
152 152 attachment.container ||= Issue.find(1)
153 153 attachment.author ||= User.find(2)
154 154 attachment.filename = @generated_filename.dup if attachment.filename.blank?
155 155 attachment.save!
156 156 attachment
157 157 end
158 158
159 159 def CustomField.generate!(attributes={})
160 160 @generated_custom_field_name ||= 'Custom field 0'
161 161 @generated_custom_field_name.succ!
162 162 field = new(attributes)
163 163 field.name = @generated_custom_field_name.dup if field.name.blank?
164 164 field.field_format = 'string' if field.field_format.blank?
165 165 yield field if block_given?
166 166 field.save!
167 167 field
168 168 end
169
170 def Changeset.generate!(attributes={})
171 @generated_changeset_rev ||= '123456'
172 @generated_changeset_rev.succ!
173 changeset = new(attributes)
174 changeset.repository ||= Project.find(1).repository
175 changeset.revision ||= @generated_changeset_rev
176 changeset.committed_on ||= Time.now
177 yield changeset if block_given?
178 changeset.save!
179 changeset
180 end
169 181 end
@@ -1,514 +1,535
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21
22 22 class ChangesetTest < ActiveSupport::TestCase
23 23 fixtures :projects, :repositories,
24 24 :issues, :issue_statuses, :issue_categories,
25 25 :changesets, :changes,
26 26 :enumerations,
27 27 :custom_fields, :custom_values,
28 28 :users, :members, :member_roles, :trackers,
29 29 :enabled_modules, :roles
30 30
31 31 def test_ref_keywords_any
32 32 ActionMailer::Base.deliveries.clear
33 33 Setting.commit_ref_keywords = '*'
34 Setting.commit_update_keywords = {'fixes , closes' => {'status_id' => '5', 'done_ratio' => '90'}}
34 Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', 'status_id' => '5', 'done_ratio' => '90'}]
35 35
36 36 c = Changeset.new(:repository => Project.find(1).repository,
37 37 :committed_on => Time.now,
38 38 :comments => 'New commit (#2). Fixes #1',
39 39 :revision => '12345')
40 40 assert c.save
41 41 assert_equal [1, 2], c.issue_ids.sort
42 42 fixed = Issue.find(1)
43 43 assert fixed.closed?
44 44 assert_equal 90, fixed.done_ratio
45 45 assert_equal 1, ActionMailer::Base.deliveries.size
46 46 end
47 47
48 48 def test_ref_keywords
49 49 Setting.commit_ref_keywords = 'refs'
50 50 Setting.commit_update_keywords = ''
51 51 c = Changeset.new(:repository => Project.find(1).repository,
52 52 :committed_on => Time.now,
53 53 :comments => 'Ignores #2. Refs #1',
54 54 :revision => '12345')
55 55 assert c.save
56 56 assert_equal [1], c.issue_ids.sort
57 57 end
58 58
59 59 def test_ref_keywords_any_only
60 60 Setting.commit_ref_keywords = '*'
61 61 Setting.commit_update_keywords = ''
62 62 c = Changeset.new(:repository => Project.find(1).repository,
63 63 :committed_on => Time.now,
64 64 :comments => 'Ignores #2. Refs #1',
65 65 :revision => '12345')
66 66 assert c.save
67 67 assert_equal [1, 2], c.issue_ids.sort
68 68 end
69 69
70 70 def test_ref_keywords_any_with_timelog
71 71 Setting.commit_ref_keywords = '*'
72 72 Setting.commit_logtime_enabled = '1'
73 73
74 74 {
75 75 '2' => 2.0,
76 76 '2h' => 2.0,
77 77 '2hours' => 2.0,
78 78 '15m' => 0.25,
79 79 '15min' => 0.25,
80 80 '3h15' => 3.25,
81 81 '3h15m' => 3.25,
82 82 '3h15min' => 3.25,
83 83 '3:15' => 3.25,
84 84 '3.25' => 3.25,
85 85 '3.25h' => 3.25,
86 86 '3,25' => 3.25,
87 87 '3,25h' => 3.25,
88 88 }.each do |syntax, expected_hours|
89 89 c = Changeset.new(:repository => Project.find(1).repository,
90 90 :committed_on => 24.hours.ago,
91 91 :comments => "Worked on this issue #1 @#{syntax}",
92 92 :revision => '520',
93 93 :user => User.find(2))
94 94 assert_difference 'TimeEntry.count' do
95 95 c.scan_comment_for_issue_ids
96 96 end
97 97 assert_equal [1], c.issue_ids.sort
98 98
99 99 time = TimeEntry.first(:order => 'id desc')
100 100 assert_equal 1, time.issue_id
101 101 assert_equal 1, time.project_id
102 102 assert_equal 2, time.user_id
103 103 assert_equal expected_hours, time.hours,
104 104 "@#{syntax} should be logged as #{expected_hours} hours but was #{time.hours}"
105 105 assert_equal Date.yesterday, time.spent_on
106 106 assert time.activity.is_default?
107 107 assert time.comments.include?('r520'),
108 108 "r520 was expected in time_entry comments: #{time.comments}"
109 109 end
110 110 end
111 111
112 112 def test_ref_keywords_closing_with_timelog
113 113 Setting.commit_ref_keywords = '*'
114 Setting.commit_update_keywords = {
115 'fixes , closes' => {
116 'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s
117 }
118 }
114 Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', 'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s}]
119 115 Setting.commit_logtime_enabled = '1'
120 116
121 117 c = Changeset.new(:repository => Project.find(1).repository,
122 118 :committed_on => Time.now,
123 119 :comments => 'This is a comment. Fixes #1 @4.5, #2 @1',
124 120 :user => User.find(2))
125 121 assert_difference 'TimeEntry.count', 2 do
126 122 c.scan_comment_for_issue_ids
127 123 end
128 124
129 125 assert_equal [1, 2], c.issue_ids.sort
130 126 assert Issue.find(1).closed?
131 127 assert Issue.find(2).closed?
132 128
133 129 times = TimeEntry.all(:order => 'id desc', :limit => 2)
134 130 assert_equal [1, 2], times.collect(&:issue_id).sort
135 131 end
136 132
137 133 def test_ref_keywords_any_line_start
138 134 Setting.commit_ref_keywords = '*'
139 135 c = Changeset.new(:repository => Project.find(1).repository,
140 136 :committed_on => Time.now,
141 137 :comments => '#1 is the reason of this commit',
142 138 :revision => '12345')
143 139 assert c.save
144 140 assert_equal [1], c.issue_ids.sort
145 141 end
146 142
147 143 def test_ref_keywords_allow_brackets_around_a_issue_number
148 144 Setting.commit_ref_keywords = '*'
149 145 c = Changeset.new(:repository => Project.find(1).repository,
150 146 :committed_on => Time.now,
151 147 :comments => '[#1] Worked on this issue',
152 148 :revision => '12345')
153 149 assert c.save
154 150 assert_equal [1], c.issue_ids.sort
155 151 end
156 152
157 153 def test_ref_keywords_allow_brackets_around_multiple_issue_numbers
158 154 Setting.commit_ref_keywords = '*'
159 155 c = Changeset.new(:repository => Project.find(1).repository,
160 156 :committed_on => Time.now,
161 157 :comments => '[#1 #2, #3] Worked on these',
162 158 :revision => '12345')
163 159 assert c.save
164 160 assert_equal [1,2,3], c.issue_ids.sort
165 161 end
166 162
167 163 def test_update_keywords_with_multiple_rules
168 Setting.commit_update_keywords = {
169 'fixes, closes' => {'status_id' => '5'},
170 'resolves' => {'status_id' => '3'}
171 }
164 with_settings :commit_update_keywords => [
165 {'keywords' => 'fixes, closes', 'status_id' => '5'},
166 {'keywords' => 'resolves', 'status_id' => '3'}
167 ] do
168
172 169 issue1 = Issue.generate!
173 170 issue2 = Issue.generate!
171 Changeset.generate!(:comments => "Closes ##{issue1.id}\nResolves ##{issue2.id}")
172 assert_equal 5, issue1.reload.status_id
173 assert_equal 3, issue2.reload.status_id
174 end
175 end
174 176
175 c = Changeset.new(:repository => Project.find(1).repository,
176 :committed_on => Time.now,
177 :comments => "Closes ##{issue1.id}\nResolves ##{issue2.id}",
178 :revision => '12345')
179 assert c.save
177 def test_update_keywords_with_multiple_rules_should_match_tracker
178 with_settings :commit_update_keywords => [
179 {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
180 {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => ''}
181 ] do
182
183 issue1 = Issue.generate!(:tracker_id => 2)
184 issue2 = Issue.generate!
185 Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
180 186 assert_equal 5, issue1.reload.status_id
181 187 assert_equal 3, issue2.reload.status_id
182 188 end
189 end
190
191 def test_update_keywords_with_multiple_rules_and_no_match
192 with_settings :commit_update_keywords => [
193 {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
194 {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => '3'}
195 ] do
196
197 issue1 = Issue.generate!(:tracker_id => 2)
198 issue2 = Issue.generate!
199 Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
200 assert_equal 5, issue1.reload.status_id
201 assert_equal 1, issue2.reload.status_id # no updates
202 end
203 end
183 204
184 205 def test_commit_referencing_a_subproject_issue
185 206 c = Changeset.new(:repository => Project.find(1).repository,
186 207 :committed_on => Time.now,
187 208 :comments => 'refs #5, a subproject issue',
188 209 :revision => '12345')
189 210 assert c.save
190 211 assert_equal [5], c.issue_ids.sort
191 212 assert c.issues.first.project != c.project
192 213 end
193 214
194 215 def test_commit_closing_a_subproject_issue
195 with_settings :commit_update_keywords => {'closes' => {'status_id' => '5'}},
216 with_settings :commit_update_keywords => [{'keywords' => 'closes', 'status_id' => '5'}],
196 217 :default_language => 'en' do
197 218 issue = Issue.find(5)
198 219 assert !issue.closed?
199 220 assert_difference 'Journal.count' do
200 221 c = Changeset.new(:repository => Project.find(1).repository,
201 222 :committed_on => Time.now,
202 223 :comments => 'closes #5, a subproject issue',
203 224 :revision => '12345')
204 225 assert c.save
205 226 end
206 227 assert issue.reload.closed?
207 228 journal = Journal.first(:order => 'id DESC')
208 229 assert_equal issue, journal.issue
209 230 assert_include "Applied in changeset ecookbook:r12345.", journal.notes
210 231 end
211 232 end
212 233
213 234 def test_commit_referencing_a_parent_project_issue
214 235 # repository of child project
215 236 r = Repository::Subversion.create!(
216 237 :project => Project.find(3),
217 238 :url => 'svn://localhost/test')
218 239 c = Changeset.new(:repository => r,
219 240 :committed_on => Time.now,
220 241 :comments => 'refs #2, an issue of a parent project',
221 242 :revision => '12345')
222 243 assert c.save
223 244 assert_equal [2], c.issue_ids.sort
224 245 assert c.issues.first.project != c.project
225 246 end
226 247
227 248 def test_commit_referencing_a_project_with_commit_cross_project_ref_disabled
228 249 r = Repository::Subversion.create!(
229 250 :project => Project.find(3),
230 251 :url => 'svn://localhost/test')
231 252
232 253 with_settings :commit_cross_project_ref => '0' do
233 254 c = Changeset.new(:repository => r,
234 255 :committed_on => Time.now,
235 256 :comments => 'refs #4, an issue of a different project',
236 257 :revision => '12345')
237 258 assert c.save
238 259 assert_equal [], c.issue_ids
239 260 end
240 261 end
241 262
242 263 def test_commit_referencing_a_project_with_commit_cross_project_ref_enabled
243 264 r = Repository::Subversion.create!(
244 265 :project => Project.find(3),
245 266 :url => 'svn://localhost/test')
246 267
247 268 with_settings :commit_cross_project_ref => '1' do
248 269 c = Changeset.new(:repository => r,
249 270 :committed_on => Time.now,
250 271 :comments => 'refs #4, an issue of a different project',
251 272 :revision => '12345')
252 273 assert c.save
253 274 assert_equal [4], c.issue_ids
254 275 end
255 276 end
256 277
257 278 def test_old_commits_should_not_update_issues_nor_log_time
258 279 Setting.commit_ref_keywords = '*'
259 280 Setting.commit_update_keywords = {'fixes , closes' => {'status_id' => '5', 'done_ratio' => '90'}}
260 281 Setting.commit_logtime_enabled = '1'
261 282
262 283 repository = Project.find(1).repository
263 284 repository.created_on = Time.now
264 285 repository.save!
265 286
266 287 c = Changeset.new(:repository => repository,
267 288 :committed_on => 1.month.ago,
268 289 :comments => 'New commit (#2). Fixes #1 @1h',
269 290 :revision => '12345')
270 291 assert_no_difference 'TimeEntry.count' do
271 292 assert c.save
272 293 end
273 294 assert_equal [1, 2], c.issue_ids.sort
274 295 issue = Issue.find(1)
275 296 assert_equal 1, issue.status_id
276 297 assert_equal 0, issue.done_ratio
277 298 end
278 299
279 300 def test_text_tag_revision
280 301 c = Changeset.new(:revision => '520')
281 302 assert_equal 'r520', c.text_tag
282 303 end
283 304
284 305 def test_text_tag_revision_with_same_project
285 306 c = Changeset.new(:revision => '520', :repository => Project.find(1).repository)
286 307 assert_equal 'r520', c.text_tag(Project.find(1))
287 308 end
288 309
289 310 def test_text_tag_revision_with_different_project
290 311 c = Changeset.new(:revision => '520', :repository => Project.find(1).repository)
291 312 assert_equal 'ecookbook:r520', c.text_tag(Project.find(2))
292 313 end
293 314
294 315 def test_text_tag_revision_with_repository_identifier
295 316 r = Repository::Subversion.create!(
296 317 :project_id => 1,
297 318 :url => 'svn://localhost/test',
298 319 :identifier => 'documents')
299 320
300 321 c = Changeset.new(:revision => '520', :repository => r)
301 322 assert_equal 'documents|r520', c.text_tag
302 323 assert_equal 'ecookbook:documents|r520', c.text_tag(Project.find(2))
303 324 end
304 325
305 326 def test_text_tag_hash
306 327 c = Changeset.new(
307 328 :scmid => '7234cb2750b63f47bff735edc50a1c0a433c2518',
308 329 :revision => '7234cb2750b63f47bff735edc50a1c0a433c2518')
309 330 assert_equal 'commit:7234cb2750b63f47bff735edc50a1c0a433c2518', c.text_tag
310 331 end
311 332
312 333 def test_text_tag_hash_with_same_project
313 334 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository)
314 335 assert_equal 'commit:7234cb27', c.text_tag(Project.find(1))
315 336 end
316 337
317 338 def test_text_tag_hash_with_different_project
318 339 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository)
319 340 assert_equal 'ecookbook:commit:7234cb27', c.text_tag(Project.find(2))
320 341 end
321 342
322 343 def test_text_tag_hash_all_number
323 344 c = Changeset.new(:scmid => '0123456789', :revision => '0123456789')
324 345 assert_equal 'commit:0123456789', c.text_tag
325 346 end
326 347
327 348 def test_previous
328 349 changeset = Changeset.find_by_revision('3')
329 350 assert_equal Changeset.find_by_revision('2'), changeset.previous
330 351 end
331 352
332 353 def test_previous_nil
333 354 changeset = Changeset.find_by_revision('1')
334 355 assert_nil changeset.previous
335 356 end
336 357
337 358 def test_next
338 359 changeset = Changeset.find_by_revision('2')
339 360 assert_equal Changeset.find_by_revision('3'), changeset.next
340 361 end
341 362
342 363 def test_next_nil
343 364 changeset = Changeset.find_by_revision('10')
344 365 assert_nil changeset.next
345 366 end
346 367
347 368 def test_comments_should_be_converted_to_utf8
348 369 proj = Project.find(3)
349 370 # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
350 371 str = "Texte encod\xe9 en ISO-8859-1."
351 372 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
352 373 r = Repository::Bazaar.create!(
353 374 :project => proj,
354 375 :url => '/tmp/test/bazaar',
355 376 :log_encoding => 'ISO-8859-1' )
356 377 assert r
357 378 c = Changeset.new(:repository => r,
358 379 :committed_on => Time.now,
359 380 :revision => '123',
360 381 :scmid => '12345',
361 382 :comments => str)
362 383 assert( c.save )
363 384 str_utf8 = "Texte encod\xc3\xa9 en ISO-8859-1."
364 385 str_utf8.force_encoding("UTF-8") if str_utf8.respond_to?(:force_encoding)
365 386 assert_equal str_utf8, c.comments
366 387 end
367 388
368 389 def test_invalid_utf8_sequences_in_comments_should_be_replaced_latin1
369 390 proj = Project.find(3)
370 391 # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
371 392 str1 = "Texte encod\xe9 en ISO-8859-1."
372 393 str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
373 394 str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
374 395 str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
375 396 r = Repository::Bazaar.create!(
376 397 :project => proj,
377 398 :url => '/tmp/test/bazaar',
378 399 :log_encoding => 'UTF-8' )
379 400 assert r
380 401 c = Changeset.new(:repository => r,
381 402 :committed_on => Time.now,
382 403 :revision => '123',
383 404 :scmid => '12345',
384 405 :comments => str1,
385 406 :committer => str2)
386 407 assert( c.save )
387 408 assert_equal "Texte encod? en ISO-8859-1.", c.comments
388 409 assert_equal "?a?b?c?d?e test", c.committer
389 410 end
390 411
391 412 def test_invalid_utf8_sequences_in_comments_should_be_replaced_ja_jis
392 413 proj = Project.find(3)
393 414 str = "test\xb5\xfetest\xb5\xfe"
394 415 if str.respond_to?(:force_encoding)
395 416 str.force_encoding('ASCII-8BIT')
396 417 end
397 418 r = Repository::Bazaar.create!(
398 419 :project => proj,
399 420 :url => '/tmp/test/bazaar',
400 421 :log_encoding => 'ISO-2022-JP' )
401 422 assert r
402 423 c = Changeset.new(:repository => r,
403 424 :committed_on => Time.now,
404 425 :revision => '123',
405 426 :scmid => '12345',
406 427 :comments => str)
407 428 assert( c.save )
408 429 assert_equal "test??test??", c.comments
409 430 end
410 431
411 432 def test_comments_should_be_converted_all_latin1_to_utf8
412 433 s1 = "\xC2\x80"
413 434 s2 = "\xc3\x82\xc2\x80"
414 435 s4 = s2.dup
415 436 if s1.respond_to?(:force_encoding)
416 437 s3 = s1.dup
417 438 s1.force_encoding('ASCII-8BIT')
418 439 s2.force_encoding('ASCII-8BIT')
419 440 s3.force_encoding('ISO-8859-1')
420 441 s4.force_encoding('UTF-8')
421 442 assert_equal s3.encode('UTF-8'), s4
422 443 end
423 444 proj = Project.find(3)
424 445 r = Repository::Bazaar.create!(
425 446 :project => proj,
426 447 :url => '/tmp/test/bazaar',
427 448 :log_encoding => 'ISO-8859-1' )
428 449 assert r
429 450 c = Changeset.new(:repository => r,
430 451 :committed_on => Time.now,
431 452 :revision => '123',
432 453 :scmid => '12345',
433 454 :comments => s1)
434 455 assert( c.save )
435 456 assert_equal s4, c.comments
436 457 end
437 458
438 459 def test_invalid_utf8_sequences_in_paths_should_be_replaced
439 460 proj = Project.find(3)
440 461 str1 = "Texte encod\xe9 en ISO-8859-1"
441 462 str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
442 463 str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
443 464 str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
444 465 r = Repository::Bazaar.create!(
445 466 :project => proj,
446 467 :url => '/tmp/test/bazaar',
447 468 :log_encoding => 'UTF-8' )
448 469 assert r
449 470 cs = Changeset.new(
450 471 :repository => r,
451 472 :committed_on => Time.now,
452 473 :revision => '123',
453 474 :scmid => '12345',
454 475 :comments => "test")
455 476 assert(cs.save)
456 477 ch = Change.new(
457 478 :changeset => cs,
458 479 :action => "A",
459 480 :path => str1,
460 481 :from_path => str2,
461 482 :from_revision => "345")
462 483 assert(ch.save)
463 484 assert_equal "Texte encod? en ISO-8859-1", ch.path
464 485 assert_equal "?a?b?c?d?e test", ch.from_path
465 486 end
466 487
467 488 def test_comments_nil
468 489 proj = Project.find(3)
469 490 r = Repository::Bazaar.create!(
470 491 :project => proj,
471 492 :url => '/tmp/test/bazaar',
472 493 :log_encoding => 'ISO-8859-1' )
473 494 assert r
474 495 c = Changeset.new(:repository => r,
475 496 :committed_on => Time.now,
476 497 :revision => '123',
477 498 :scmid => '12345',
478 499 :comments => nil,
479 500 :committer => nil)
480 501 assert( c.save )
481 502 assert_equal "", c.comments
482 503 assert_equal nil, c.committer
483 504 if c.comments.respond_to?(:force_encoding)
484 505 assert_equal "UTF-8", c.comments.encoding.to_s
485 506 end
486 507 end
487 508
488 509 def test_comments_empty
489 510 proj = Project.find(3)
490 511 r = Repository::Bazaar.create!(
491 512 :project => proj,
492 513 :url => '/tmp/test/bazaar',
493 514 :log_encoding => 'ISO-8859-1' )
494 515 assert r
495 516 c = Changeset.new(:repository => r,
496 517 :committed_on => Time.now,
497 518 :revision => '123',
498 519 :scmid => '12345',
499 520 :comments => "",
500 521 :committer => "")
501 522 assert( c.save )
502 523 assert_equal "", c.comments
503 524 assert_equal "", c.committer
504 525 if c.comments.respond_to?(:force_encoding)
505 526 assert_equal "UTF-8", c.comments.encoding.to_s
506 527 assert_equal "UTF-8", c.committer.encoding.to_s
507 528 end
508 529 end
509 530
510 531 def test_identifier
511 532 c = Changeset.find_by_revision('1')
512 533 assert_equal c.revision, c.identifier
513 534 end
514 535 end
@@ -1,370 +1,370
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositoryTest < ActiveSupport::TestCase
21 21 fixtures :projects,
22 22 :trackers,
23 23 :projects_trackers,
24 24 :enabled_modules,
25 25 :repositories,
26 26 :issues,
27 27 :issue_statuses,
28 28 :issue_categories,
29 29 :changesets,
30 30 :changes,
31 31 :users,
32 32 :members,
33 33 :member_roles,
34 34 :roles,
35 35 :enumerations
36 36
37 37 include Redmine::I18n
38 38
39 39 def setup
40 40 @repository = Project.find(1).repository
41 41 end
42 42
43 43 def test_blank_log_encoding_error_message
44 44 set_language_if_valid 'en'
45 45 repo = Repository::Bazaar.new(
46 46 :project => Project.find(3),
47 47 :url => "/test",
48 48 :log_encoding => ''
49 49 )
50 50 assert !repo.save
51 51 assert_include "Commit messages encoding can't be blank",
52 52 repo.errors.full_messages
53 53 end
54 54
55 55 def test_blank_log_encoding_error_message_fr
56 56 set_language_if_valid 'fr'
57 57 str = "Encodage des messages de commit doit \xc3\xaatre renseign\xc3\xa9(e)"
58 58 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
59 59 repo = Repository::Bazaar.new(
60 60 :project => Project.find(3),
61 61 :url => "/test"
62 62 )
63 63 assert !repo.save
64 64 assert_include str, repo.errors.full_messages
65 65 end
66 66
67 67 def test_create
68 68 repository = Repository::Subversion.new(:project => Project.find(3))
69 69 assert !repository.save
70 70
71 71 repository.url = "svn://localhost"
72 72 assert repository.save
73 73 repository.reload
74 74
75 75 project = Project.find(3)
76 76 assert_equal repository, project.repository
77 77 end
78 78
79 79 def test_first_repository_should_be_set_as_default
80 80 repository1 = Repository::Subversion.new(
81 81 :project => Project.find(3),
82 82 :identifier => 'svn1',
83 83 :url => 'file:///svn1'
84 84 )
85 85 assert repository1.save
86 86 assert repository1.is_default?
87 87
88 88 repository2 = Repository::Subversion.new(
89 89 :project => Project.find(3),
90 90 :identifier => 'svn2',
91 91 :url => 'file:///svn2'
92 92 )
93 93 assert repository2.save
94 94 assert !repository2.is_default?
95 95
96 96 assert_equal repository1, Project.find(3).repository
97 97 assert_equal [repository1, repository2], Project.find(3).repositories.sort
98 98 end
99 99
100 100 def test_identifier_should_accept_letters_digits_dashes_and_underscores
101 101 r = Repository::Subversion.new(
102 102 :project_id => 3,
103 103 :identifier => 'svn-123_45',
104 104 :url => 'file:///svn'
105 105 )
106 106 assert r.save
107 107 end
108 108
109 109 def test_identifier_should_not_be_frozen_for_a_new_repository
110 110 assert_equal false, Repository.new.identifier_frozen?
111 111 end
112 112
113 113 def test_identifier_should_not_be_frozen_for_a_saved_repository_with_blank_identifier
114 114 Repository.update_all(["identifier = ''"], "id = 10")
115 115
116 116 assert_equal false, Repository.find(10).identifier_frozen?
117 117 end
118 118
119 119 def test_identifier_should_be_frozen_for_a_saved_repository_with_valid_identifier
120 120 Repository.update_all(["identifier = 'abc123'"], "id = 10")
121 121
122 122 assert_equal true, Repository.find(10).identifier_frozen?
123 123 end
124 124
125 125 def test_identifier_should_not_accept_change_if_frozen
126 126 r = Repository.new(:identifier => 'foo')
127 127 r.stubs(:identifier_frozen?).returns(true)
128 128
129 129 r.identifier = 'bar'
130 130 assert_equal 'foo', r.identifier
131 131 end
132 132
133 133 def test_identifier_should_accept_change_if_not_frozen
134 134 r = Repository.new(:identifier => 'foo')
135 135 r.stubs(:identifier_frozen?).returns(false)
136 136
137 137 r.identifier = 'bar'
138 138 assert_equal 'bar', r.identifier
139 139 end
140 140
141 141 def test_destroy
142 142 repository = Repository.find(10)
143 143 changesets = repository.changesets.count
144 144 changes = repository.filechanges.count
145 145
146 146 assert_difference 'Changeset.count', -changesets do
147 147 assert_difference 'Change.count', -changes do
148 148 Repository.find(10).destroy
149 149 end
150 150 end
151 151 end
152 152
153 153 def test_destroy_should_delete_parents_associations
154 154 changeset = Changeset.find(102)
155 155 changeset.parents = Changeset.find_all_by_id([100, 101])
156 156
157 157 assert_difference 'Changeset.connection.select_all("select * from changeset_parents").size', -2 do
158 158 Repository.find(10).destroy
159 159 end
160 160 end
161 161
162 162 def test_destroy_should_delete_issues_associations
163 163 changeset = Changeset.find(102)
164 164 changeset.issues = Issue.find_all_by_id([1, 2])
165 165
166 166 assert_difference 'Changeset.connection.select_all("select * from changesets_issues").size', -2 do
167 167 Repository.find(10).destroy
168 168 end
169 169 end
170 170
171 171 def test_should_not_create_with_disabled_scm
172 172 # disable Subversion
173 173 with_settings :enabled_scm => ['Darcs', 'Git'] do
174 174 repository = Repository::Subversion.new(
175 175 :project => Project.find(3), :url => "svn://localhost")
176 176 assert !repository.save
177 177 assert_include I18n.translate('activerecord.errors.messages.invalid'),
178 178 repository.errors[:type]
179 179 end
180 180 end
181 181
182 182 def test_scan_changesets_for_issue_ids
183 183 Setting.default_language = 'en'
184 184
185 185 Setting.commit_ref_keywords = 'refs , references, IssueID'
186 Setting.commit_update_keywords = {
187 'fixes , closes' => {'status_id' => IssueStatus.where(:is_closed => true).first.id, 'done_ratio' => '90'}
188 }
186 Setting.commit_update_keywords = [
187 {'keywords' => 'fixes , closes', 'status_id' => IssueStatus.where(:is_closed => true).first.id, 'done_ratio' => '90'}
188 ]
189 189 Setting.default_language = 'en'
190 190 ActionMailer::Base.deliveries.clear
191 191
192 192 # make sure issue 1 is not already closed
193 193 fixed_issue = Issue.find(1)
194 194 assert !fixed_issue.status.is_closed?
195 195 old_status = fixed_issue.status
196 196
197 197 with_settings :notified_events => %w(issue_added issue_updated) do
198 198 Repository.scan_changesets_for_issue_ids
199 199 end
200 200 assert_equal [101, 102], Issue.find(3).changeset_ids
201 201
202 202 # fixed issues
203 203 fixed_issue.reload
204 204 assert fixed_issue.status.is_closed?
205 205 assert_equal 90, fixed_issue.done_ratio
206 206 assert_equal [101], fixed_issue.changeset_ids
207 207
208 208 # issue change
209 209 journal = fixed_issue.journals.reorder('created_on desc').first
210 210 assert_equal User.find_by_login('dlopper'), journal.user
211 211 assert_equal 'Applied in changeset r2.', journal.notes
212 212
213 213 # 2 email notifications
214 214 assert_equal 2, ActionMailer::Base.deliveries.size
215 215 mail = ActionMailer::Base.deliveries.first
216 216 assert_not_nil mail
217 217 assert mail.subject.starts_with?(
218 218 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
219 219 assert_mail_body_match(
220 220 "Status changed from #{old_status} to #{fixed_issue.status}", mail)
221 221
222 222 # ignoring commits referencing an issue of another project
223 223 assert_equal [], Issue.find(4).changesets
224 224 end
225 225
226 226 def test_for_changeset_comments_strip
227 227 repository = Repository::Mercurial.create(
228 228 :project => Project.find( 4 ),
229 229 :url => '/foo/bar/baz' )
230 230 comment = <<-COMMENT
231 231 This is a loooooooooooooooooooooooooooong comment
232 232
233 233
234 234 COMMENT
235 235 changeset = Changeset.new(
236 236 :comments => comment, :commit_date => Time.now,
237 237 :revision => 0, :scmid => 'f39b7922fb3c',
238 238 :committer => 'foo <foo@example.com>',
239 239 :committed_on => Time.now, :repository => repository )
240 240 assert( changeset.save )
241 241 assert_not_equal( comment, changeset.comments )
242 242 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
243 243 changeset.comments )
244 244 end
245 245
246 246 def test_for_urls_strip_cvs
247 247 repository = Repository::Cvs.create(
248 248 :project => Project.find(4),
249 249 :url => ' :pserver:login:password@host:/path/to/the/repository',
250 250 :root_url => 'foo ',
251 251 :log_encoding => 'UTF-8')
252 252 assert repository.save
253 253 repository.reload
254 254 assert_equal ':pserver:login:password@host:/path/to/the/repository',
255 255 repository.url
256 256 assert_equal 'foo', repository.root_url
257 257 end
258 258
259 259 def test_for_urls_strip_subversion
260 260 repository = Repository::Subversion.create(
261 261 :project => Project.find(4),
262 262 :url => ' file:///dummy ')
263 263 assert repository.save
264 264 repository.reload
265 265 assert_equal 'file:///dummy', repository.url
266 266 end
267 267
268 268 def test_for_urls_strip_git
269 269 repository = Repository::Git.create(
270 270 :project => Project.find(4),
271 271 :url => ' c:\dummy ')
272 272 assert repository.save
273 273 repository.reload
274 274 assert_equal 'c:\dummy', repository.url
275 275 end
276 276
277 277 def test_manual_user_mapping
278 278 assert_no_difference "Changeset.where('user_id <> 2').count" do
279 279 c = Changeset.create!(
280 280 :repository => @repository,
281 281 :committer => 'foo',
282 282 :committed_on => Time.now,
283 283 :revision => 100,
284 284 :comments => 'Committed by foo.'
285 285 )
286 286 assert_nil c.user
287 287 @repository.committer_ids = {'foo' => '2'}
288 288 assert_equal User.find(2), c.reload.user
289 289 # committer is now mapped
290 290 c = Changeset.create!(
291 291 :repository => @repository,
292 292 :committer => 'foo',
293 293 :committed_on => Time.now,
294 294 :revision => 101,
295 295 :comments => 'Another commit by foo.'
296 296 )
297 297 assert_equal User.find(2), c.user
298 298 end
299 299 end
300 300
301 301 def test_auto_user_mapping_by_username
302 302 c = Changeset.create!(
303 303 :repository => @repository,
304 304 :committer => 'jsmith',
305 305 :committed_on => Time.now,
306 306 :revision => 100,
307 307 :comments => 'Committed by john.'
308 308 )
309 309 assert_equal User.find(2), c.user
310 310 end
311 311
312 312 def test_auto_user_mapping_by_email
313 313 c = Changeset.create!(
314 314 :repository => @repository,
315 315 :committer => 'john <jsmith@somenet.foo>',
316 316 :committed_on => Time.now,
317 317 :revision => 100,
318 318 :comments => 'Committed by john.'
319 319 )
320 320 assert_equal User.find(2), c.user
321 321 end
322 322
323 323 def test_filesystem_avaialbe
324 324 klass = Repository::Filesystem
325 325 assert klass.scm_adapter_class
326 326 assert_equal true, klass.scm_available
327 327 end
328 328
329 329 def test_merge_extra_info
330 330 repo = Repository::Subversion.new(:project => Project.find(3))
331 331 assert !repo.save
332 332 repo.url = "svn://localhost"
333 333 assert repo.save
334 334 repo.reload
335 335 project = Project.find(3)
336 336 assert_equal repo, project.repository
337 337 assert_nil repo.extra_info
338 338 h1 = {"test_1" => {"test_11" => "test_value_11"}}
339 339 repo.merge_extra_info(h1)
340 340 assert_equal h1, repo.extra_info
341 341 h2 = {"test_2" => {
342 342 "test_21" => "test_value_21",
343 343 "test_22" => "test_value_22",
344 344 }}
345 345 repo.merge_extra_info(h2)
346 346 assert_equal (h = {"test_11" => "test_value_11"}),
347 347 repo.extra_info["test_1"]
348 348 assert_equal "test_value_21",
349 349 repo.extra_info["test_2"]["test_21"]
350 350 h3 = {"test_2" => {
351 351 "test_23" => "test_value_23",
352 352 "test_24" => "test_value_24",
353 353 }}
354 354 repo.merge_extra_info(h3)
355 355 assert_equal (h = {"test_11" => "test_value_11"}),
356 356 repo.extra_info["test_1"]
357 357 assert_nil repo.extra_info["test_2"]["test_21"]
358 358 assert_equal "test_value_23",
359 359 repo.extra_info["test_2"]["test_23"]
360 360 end
361 361
362 362 def test_sort_should_not_raise_an_error_with_nil_identifiers
363 363 r1 = Repository.new
364 364 r2 = Repository.new
365 365
366 366 assert_nothing_raised do
367 367 [r1, r2].sort
368 368 end
369 369 end
370 370 end
General Comments 0
You need to be logged in to leave comments. Login now