##// END OF EJS Templates
Don't link multiple changesets from the same commit multiple times (#17931)....
Jean-Philippe Lang -
r13063:a56754633520
parent child
Show More
@@ -1,283 +1,289
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 class Changeset < ActiveRecord::Base
18 class Changeset < ActiveRecord::Base
19 belongs_to :repository
19 belongs_to :repository
20 belongs_to :user
20 belongs_to :user
21 has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
21 has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
22 has_and_belongs_to_many :issues
22 has_and_belongs_to_many :issues
23 has_and_belongs_to_many :parents,
23 has_and_belongs_to_many :parents,
24 :class_name => "Changeset",
24 :class_name => "Changeset",
25 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
25 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
26 :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
26 :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
27 has_and_belongs_to_many :children,
27 has_and_belongs_to_many :children,
28 :class_name => "Changeset",
28 :class_name => "Changeset",
29 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
29 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
30 :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
30 :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
31
31
32 acts_as_event :title => Proc.new {|o| o.title},
32 acts_as_event :title => Proc.new {|o| o.title},
33 :description => :long_comments,
33 :description => :long_comments,
34 :datetime => :committed_on,
34 :datetime => :committed_on,
35 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
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 acts_as_searchable :columns => 'comments',
37 acts_as_searchable :columns => 'comments',
38 :include => {:repository => :project},
38 :include => {:repository => :project},
39 :project_key => "#{Repository.table_name}.project_id",
39 :project_key => "#{Repository.table_name}.project_id",
40 :date_column => 'committed_on'
40 :date_column => 'committed_on'
41
41
42 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
42 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
43 :author_key => :user_id,
43 :author_key => :user_id,
44 :find_options => {:include => [:user, {:repository => :project}]}
44 :find_options => {:include => [:user, {:repository => :project}]}
45
45
46 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
46 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
47 validates_uniqueness_of :revision, :scope => :repository_id
47 validates_uniqueness_of :revision, :scope => :repository_id
48 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
48 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
49
49
50 scope :visible, lambda {|*args|
50 scope :visible, lambda {|*args|
51 includes(:repository => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
51 includes(:repository => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
52 }
52 }
53
53
54 after_create :scan_for_issues
54 after_create :scan_for_issues
55 before_create :before_create_cs
55 before_create :before_create_cs
56
56
57 def revision=(r)
57 def revision=(r)
58 write_attribute :revision, (r.nil? ? nil : r.to_s)
58 write_attribute :revision, (r.nil? ? nil : r.to_s)
59 end
59 end
60
60
61 # Returns the identifier of this changeset; depending on repository backends
61 # Returns the identifier of this changeset; depending on repository backends
62 def identifier
62 def identifier
63 if repository.class.respond_to? :changeset_identifier
63 if repository.class.respond_to? :changeset_identifier
64 repository.class.changeset_identifier self
64 repository.class.changeset_identifier self
65 else
65 else
66 revision.to_s
66 revision.to_s
67 end
67 end
68 end
68 end
69
69
70 def committed_on=(date)
70 def committed_on=(date)
71 self.commit_date = date
71 self.commit_date = date
72 super
72 super
73 end
73 end
74
74
75 # Returns the readable identifier
75 # Returns the readable identifier
76 def format_identifier
76 def format_identifier
77 if repository.class.respond_to? :format_changeset_identifier
77 if repository.class.respond_to? :format_changeset_identifier
78 repository.class.format_changeset_identifier self
78 repository.class.format_changeset_identifier self
79 else
79 else
80 identifier
80 identifier
81 end
81 end
82 end
82 end
83
83
84 def project
84 def project
85 repository.project
85 repository.project
86 end
86 end
87
87
88 def author
88 def author
89 user || committer.to_s.split('<').first
89 user || committer.to_s.split('<').first
90 end
90 end
91
91
92 def before_create_cs
92 def before_create_cs
93 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
93 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
94 self.comments = self.class.normalize_comments(
94 self.comments = self.class.normalize_comments(
95 self.comments, repository.repo_log_encoding)
95 self.comments, repository.repo_log_encoding)
96 self.user = repository.find_committer_user(self.committer)
96 self.user = repository.find_committer_user(self.committer)
97 end
97 end
98
98
99 def scan_for_issues
99 def scan_for_issues
100 scan_comment_for_issue_ids
100 scan_comment_for_issue_ids
101 end
101 end
102
102
103 TIMELOG_RE = /
103 TIMELOG_RE = /
104 (
104 (
105 ((\d+)(h|hours?))((\d+)(m|min)?)?
105 ((\d+)(h|hours?))((\d+)(m|min)?)?
106 |
106 |
107 ((\d+)(h|hours?|m|min))
107 ((\d+)(h|hours?|m|min))
108 |
108 |
109 (\d+):(\d+)
109 (\d+):(\d+)
110 |
110 |
111 (\d+([\.,]\d+)?)h?
111 (\d+([\.,]\d+)?)h?
112 )
112 )
113 /x
113 /x
114
114
115 def scan_comment_for_issue_ids
115 def scan_comment_for_issue_ids
116 return if comments.blank?
116 return if comments.blank?
117 # keywords used to reference issues
117 # keywords used to reference issues
118 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
118 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
119 ref_keywords_any = ref_keywords.delete('*')
119 ref_keywords_any = ref_keywords.delete('*')
120 # keywords used to fix issues
120 # keywords used to fix issues
121 fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
121 fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
122
122
123 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
123 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
124
124
125 referenced_issues = []
125 referenced_issues = []
126
126
127 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
127 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
128 action, refs = match[2].to_s.downcase, match[3]
128 action, refs = match[2].to_s.downcase, match[3]
129 next unless action.present? || ref_keywords_any
129 next unless action.present? || ref_keywords_any
130
130
131 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
131 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
132 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
132 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
133 if issue
133 if issue && !issue_linked_to_same_commit?(issue)
134 referenced_issues << issue
134 referenced_issues << issue
135 # Don't update issues or log time when importing old commits
135 # Don't update issues or log time when importing old commits
136 unless repository.created_on && committed_on && committed_on < repository.created_on
136 unless repository.created_on && committed_on && committed_on < repository.created_on
137 fix_issue(issue, action) if fix_keywords.include?(action)
137 fix_issue(issue, action) if fix_keywords.include?(action)
138 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
138 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
139 end
139 end
140 end
140 end
141 end
141 end
142 end
142 end
143
143
144 referenced_issues.uniq!
144 referenced_issues.uniq!
145 self.issues = referenced_issues unless referenced_issues.empty?
145 self.issues = referenced_issues unless referenced_issues.empty?
146 end
146 end
147
147
148 def short_comments
148 def short_comments
149 @short_comments || split_comments.first
149 @short_comments || split_comments.first
150 end
150 end
151
151
152 def long_comments
152 def long_comments
153 @long_comments || split_comments.last
153 @long_comments || split_comments.last
154 end
154 end
155
155
156 def text_tag(ref_project=nil)
156 def text_tag(ref_project=nil)
157 repo = ""
157 repo = ""
158 if repository && repository.identifier.present?
158 if repository && repository.identifier.present?
159 repo = "#{repository.identifier}|"
159 repo = "#{repository.identifier}|"
160 end
160 end
161 tag = if scmid?
161 tag = if scmid?
162 "commit:#{repo}#{scmid}"
162 "commit:#{repo}#{scmid}"
163 else
163 else
164 "#{repo}r#{revision}"
164 "#{repo}r#{revision}"
165 end
165 end
166 if ref_project && project && ref_project != project
166 if ref_project && project && ref_project != project
167 tag = "#{project.identifier}:#{tag}"
167 tag = "#{project.identifier}:#{tag}"
168 end
168 end
169 tag
169 tag
170 end
170 end
171
171
172 # Returns the title used for the changeset in the activity/search results
172 # Returns the title used for the changeset in the activity/search results
173 def title
173 def title
174 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
174 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
175 comm = short_comments.blank? ? '' : (': ' + short_comments)
175 comm = short_comments.blank? ? '' : (': ' + short_comments)
176 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
176 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
177 end
177 end
178
178
179 # Returns the previous changeset
179 # Returns the previous changeset
180 def previous
180 def previous
181 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
181 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
182 end
182 end
183
183
184 # Returns the next changeset
184 # Returns the next changeset
185 def next
185 def next
186 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
186 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
187 end
187 end
188
188
189 # Creates a new Change from it's common parameters
189 # Creates a new Change from it's common parameters
190 def create_change(change)
190 def create_change(change)
191 Change.create(:changeset => self,
191 Change.create(:changeset => self,
192 :action => change[:action],
192 :action => change[:action],
193 :path => change[:path],
193 :path => change[:path],
194 :from_path => change[:from_path],
194 :from_path => change[:from_path],
195 :from_revision => change[:from_revision])
195 :from_revision => change[:from_revision])
196 end
196 end
197
197
198 # Finds an issue that can be referenced by the commit message
198 # Finds an issue that can be referenced by the commit message
199 def find_referenced_issue_by_id(id)
199 def find_referenced_issue_by_id(id)
200 return nil if id.blank?
200 return nil if id.blank?
201 issue = Issue.includes(:project).where(:id => id.to_i).first
201 issue = Issue.includes(:project).where(:id => id.to_i).first
202 if Setting.commit_cross_project_ref?
202 if Setting.commit_cross_project_ref?
203 # all issues can be referenced/fixed
203 # all issues can be referenced/fixed
204 elsif issue
204 elsif issue
205 # issue that belong to the repository project, a subproject or a parent project only
205 # issue that belong to the repository project, a subproject or a parent project only
206 unless issue.project &&
206 unless issue.project &&
207 (project == issue.project || project.is_ancestor_of?(issue.project) ||
207 (project == issue.project || project.is_ancestor_of?(issue.project) ||
208 project.is_descendant_of?(issue.project))
208 project.is_descendant_of?(issue.project))
209 issue = nil
209 issue = nil
210 end
210 end
211 end
211 end
212 issue
212 issue
213 end
213 end
214
214
215 private
215 private
216
216
217 # Returns true if the issue is already linked to the same commit
218 # from a different repository
219 def issue_linked_to_same_commit?(issue)
220 repository.same_commits_in_scope(issue.changesets, self).any?
221 end
222
217 # Updates the +issue+ according to +action+
223 # Updates the +issue+ according to +action+
218 def fix_issue(issue, action)
224 def fix_issue(issue, action)
219 # the issue may have been updated by the closure of another one (eg. duplicate)
225 # the issue may have been updated by the closure of another one (eg. duplicate)
220 issue.reload
226 issue.reload
221 # don't change the status is the issue is closed
227 # don't change the status is the issue is closed
222 return if issue.status && issue.status.is_closed?
228 return if issue.status && issue.status.is_closed?
223
229
224 journal = issue.init_journal(user || User.anonymous,
230 journal = issue.init_journal(user || User.anonymous,
225 ll(Setting.default_language,
231 ll(Setting.default_language,
226 :text_status_changed_by_changeset,
232 :text_status_changed_by_changeset,
227 text_tag(issue.project)))
233 text_tag(issue.project)))
228 rule = Setting.commit_update_keywords_array.detect do |rule|
234 rule = Setting.commit_update_keywords_array.detect do |rule|
229 rule['keywords'].include?(action) &&
235 rule['keywords'].include?(action) &&
230 (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
236 (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
231 end
237 end
232 if rule
238 if rule
233 issue.assign_attributes rule.slice(*Issue.attribute_names)
239 issue.assign_attributes rule.slice(*Issue.attribute_names)
234 end
240 end
235 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
241 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
236 { :changeset => self, :issue => issue, :action => action })
242 { :changeset => self, :issue => issue, :action => action })
237 unless issue.save
243 unless issue.save
238 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
244 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
239 end
245 end
240 issue
246 issue
241 end
247 end
242
248
243 def log_time(issue, hours)
249 def log_time(issue, hours)
244 time_entry = TimeEntry.new(
250 time_entry = TimeEntry.new(
245 :user => user,
251 :user => user,
246 :hours => hours,
252 :hours => hours,
247 :issue => issue,
253 :issue => issue,
248 :spent_on => commit_date,
254 :spent_on => commit_date,
249 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
255 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
250 :locale => Setting.default_language)
256 :locale => Setting.default_language)
251 )
257 )
252 time_entry.activity = log_time_activity unless log_time_activity.nil?
258 time_entry.activity = log_time_activity unless log_time_activity.nil?
253
259
254 unless time_entry.save
260 unless time_entry.save
255 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
261 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
256 end
262 end
257 time_entry
263 time_entry
258 end
264 end
259
265
260 def log_time_activity
266 def log_time_activity
261 if Setting.commit_logtime_activity_id.to_i > 0
267 if Setting.commit_logtime_activity_id.to_i > 0
262 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
268 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
263 end
269 end
264 end
270 end
265
271
266 def split_comments
272 def split_comments
267 comments =~ /\A(.+?)\r?\n(.*)$/m
273 comments =~ /\A(.+?)\r?\n(.*)$/m
268 @short_comments = $1 || comments
274 @short_comments = $1 || comments
269 @long_comments = $2.to_s.strip
275 @long_comments = $2.to_s.strip
270 return @short_comments, @long_comments
276 return @short_comments, @long_comments
271 end
277 end
272
278
273 public
279 public
274
280
275 # Strips and reencodes a commit log before insertion into the database
281 # Strips and reencodes a commit log before insertion into the database
276 def self.normalize_comments(str, encoding)
282 def self.normalize_comments(str, encoding)
277 Changeset.to_utf8(str.to_s.strip, encoding)
283 Changeset.to_utf8(str.to_s.strip, encoding)
278 end
284 end
279
285
280 def self.to_utf8(str, encoding)
286 def self.to_utf8(str, encoding)
281 Redmine::CodesetUtil.to_utf8(str, encoding)
287 Redmine::CodesetUtil.to_utf8(str, encoding)
282 end
288 end
283 end
289 end
@@ -1,484 +1,496
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 class ScmFetchError < Exception; end
18 class ScmFetchError < Exception; end
19
19
20 class Repository < ActiveRecord::Base
20 class Repository < ActiveRecord::Base
21 include Redmine::Ciphering
21 include Redmine::Ciphering
22 include Redmine::SafeAttributes
22 include Redmine::SafeAttributes
23
23
24 # Maximum length for repository identifiers
24 # Maximum length for repository identifiers
25 IDENTIFIER_MAX_LENGTH = 255
25 IDENTIFIER_MAX_LENGTH = 255
26
26
27 belongs_to :project
27 belongs_to :project
28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
29 has_many :filechanges, :class_name => 'Change', :through => :changesets
29 has_many :filechanges, :class_name => 'Change', :through => :changesets
30
30
31 serialize :extra_info
31 serialize :extra_info
32
32
33 before_save :check_default
33 before_save :check_default
34
34
35 # Raw SQL to delete changesets and changes in the database
35 # Raw SQL to delete changesets and changes in the database
36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
37 before_destroy :clear_changesets
37 before_destroy :clear_changesets
38
38
39 validates_length_of :password, :maximum => 255, :allow_nil => true
39 validates_length_of :password, :maximum => 255, :allow_nil => true
40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
43 validates_exclusion_of :identifier, :in => %w(browse show entry raw changes annotate diff statistics graph revisions revision)
43 validates_exclusion_of :identifier, :in => %w(browse show entry raw changes annotate diff statistics graph revisions revision)
44 # donwcase letters, digits, dashes, underscores but not digits only
44 # donwcase letters, digits, dashes, underscores but not digits only
45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
46 # Checks if the SCM is enabled when creating a repository
46 # Checks if the SCM is enabled when creating a repository
47 validate :repo_create_validation, :on => :create
47 validate :repo_create_validation, :on => :create
48
48
49 safe_attributes 'identifier',
49 safe_attributes 'identifier',
50 'login',
50 'login',
51 'password',
51 'password',
52 'path_encoding',
52 'path_encoding',
53 'log_encoding',
53 'log_encoding',
54 'is_default'
54 'is_default'
55
55
56 safe_attributes 'url',
56 safe_attributes 'url',
57 :if => lambda {|repository, user| repository.new_record?}
57 :if => lambda {|repository, user| repository.new_record?}
58
58
59 def repo_create_validation
59 def repo_create_validation
60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
61 errors.add(:type, :invalid)
61 errors.add(:type, :invalid)
62 end
62 end
63 end
63 end
64
64
65 def self.human_attribute_name(attribute_key_name, *args)
65 def self.human_attribute_name(attribute_key_name, *args)
66 attr_name = attribute_key_name.to_s
66 attr_name = attribute_key_name.to_s
67 if attr_name == "log_encoding"
67 if attr_name == "log_encoding"
68 attr_name = "commit_logs_encoding"
68 attr_name = "commit_logs_encoding"
69 end
69 end
70 super(attr_name, *args)
70 super(attr_name, *args)
71 end
71 end
72
72
73 # Removes leading and trailing whitespace
73 # Removes leading and trailing whitespace
74 def url=(arg)
74 def url=(arg)
75 write_attribute(:url, arg ? arg.to_s.strip : nil)
75 write_attribute(:url, arg ? arg.to_s.strip : nil)
76 end
76 end
77
77
78 # Removes leading and trailing whitespace
78 # Removes leading and trailing whitespace
79 def root_url=(arg)
79 def root_url=(arg)
80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
81 end
81 end
82
82
83 def password
83 def password
84 read_ciphered_attribute(:password)
84 read_ciphered_attribute(:password)
85 end
85 end
86
86
87 def password=(arg)
87 def password=(arg)
88 write_ciphered_attribute(:password, arg)
88 write_ciphered_attribute(:password, arg)
89 end
89 end
90
90
91 def scm_adapter
91 def scm_adapter
92 self.class.scm_adapter_class
92 self.class.scm_adapter_class
93 end
93 end
94
94
95 def scm
95 def scm
96 unless @scm
96 unless @scm
97 @scm = self.scm_adapter.new(url, root_url,
97 @scm = self.scm_adapter.new(url, root_url,
98 login, password, path_encoding)
98 login, password, path_encoding)
99 if root_url.blank? && @scm.root_url.present?
99 if root_url.blank? && @scm.root_url.present?
100 update_attribute(:root_url, @scm.root_url)
100 update_attribute(:root_url, @scm.root_url)
101 end
101 end
102 end
102 end
103 @scm
103 @scm
104 end
104 end
105
105
106 def scm_name
106 def scm_name
107 self.class.scm_name
107 self.class.scm_name
108 end
108 end
109
109
110 def name
110 def name
111 if identifier.present?
111 if identifier.present?
112 identifier
112 identifier
113 elsif is_default?
113 elsif is_default?
114 l(:field_repository_is_default)
114 l(:field_repository_is_default)
115 else
115 else
116 scm_name
116 scm_name
117 end
117 end
118 end
118 end
119
119
120 def identifier=(identifier)
120 def identifier=(identifier)
121 super unless identifier_frozen?
121 super unless identifier_frozen?
122 end
122 end
123
123
124 def identifier_frozen?
124 def identifier_frozen?
125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
126 end
126 end
127
127
128 def identifier_param
128 def identifier_param
129 if is_default?
129 if is_default?
130 nil
130 nil
131 elsif identifier.present?
131 elsif identifier.present?
132 identifier
132 identifier
133 else
133 else
134 id.to_s
134 id.to_s
135 end
135 end
136 end
136 end
137
137
138 def <=>(repository)
138 def <=>(repository)
139 if is_default?
139 if is_default?
140 -1
140 -1
141 elsif repository.is_default?
141 elsif repository.is_default?
142 1
142 1
143 else
143 else
144 identifier.to_s <=> repository.identifier.to_s
144 identifier.to_s <=> repository.identifier.to_s
145 end
145 end
146 end
146 end
147
147
148 def self.find_by_identifier_param(param)
148 def self.find_by_identifier_param(param)
149 if param.to_s =~ /^\d+$/
149 if param.to_s =~ /^\d+$/
150 find_by_id(param)
150 find_by_id(param)
151 else
151 else
152 find_by_identifier(param)
152 find_by_identifier(param)
153 end
153 end
154 end
154 end
155
155
156 # TODO: should return an empty hash instead of nil to avoid many ||{}
156 # TODO: should return an empty hash instead of nil to avoid many ||{}
157 def extra_info
157 def extra_info
158 h = read_attribute(:extra_info)
158 h = read_attribute(:extra_info)
159 h.is_a?(Hash) ? h : nil
159 h.is_a?(Hash) ? h : nil
160 end
160 end
161
161
162 def merge_extra_info(arg)
162 def merge_extra_info(arg)
163 h = extra_info || {}
163 h = extra_info || {}
164 return h if arg.nil?
164 return h if arg.nil?
165 h.merge!(arg)
165 h.merge!(arg)
166 write_attribute(:extra_info, h)
166 write_attribute(:extra_info, h)
167 end
167 end
168
168
169 def report_last_commit
169 def report_last_commit
170 true
170 true
171 end
171 end
172
172
173 def supports_cat?
173 def supports_cat?
174 scm.supports_cat?
174 scm.supports_cat?
175 end
175 end
176
176
177 def supports_annotate?
177 def supports_annotate?
178 scm.supports_annotate?
178 scm.supports_annotate?
179 end
179 end
180
180
181 def supports_all_revisions?
181 def supports_all_revisions?
182 true
182 true
183 end
183 end
184
184
185 def supports_directory_revisions?
185 def supports_directory_revisions?
186 false
186 false
187 end
187 end
188
188
189 def supports_revision_graph?
189 def supports_revision_graph?
190 false
190 false
191 end
191 end
192
192
193 def entry(path=nil, identifier=nil)
193 def entry(path=nil, identifier=nil)
194 scm.entry(path, identifier)
194 scm.entry(path, identifier)
195 end
195 end
196
196
197 def scm_entries(path=nil, identifier=nil)
197 def scm_entries(path=nil, identifier=nil)
198 scm.entries(path, identifier)
198 scm.entries(path, identifier)
199 end
199 end
200 protected :scm_entries
200 protected :scm_entries
201
201
202 def entries(path=nil, identifier=nil)
202 def entries(path=nil, identifier=nil)
203 entries = scm_entries(path, identifier)
203 entries = scm_entries(path, identifier)
204 load_entries_changesets(entries)
204 load_entries_changesets(entries)
205 entries
205 entries
206 end
206 end
207
207
208 def branches
208 def branches
209 scm.branches
209 scm.branches
210 end
210 end
211
211
212 def tags
212 def tags
213 scm.tags
213 scm.tags
214 end
214 end
215
215
216 def default_branch
216 def default_branch
217 nil
217 nil
218 end
218 end
219
219
220 def properties(path, identifier=nil)
220 def properties(path, identifier=nil)
221 scm.properties(path, identifier)
221 scm.properties(path, identifier)
222 end
222 end
223
223
224 def cat(path, identifier=nil)
224 def cat(path, identifier=nil)
225 scm.cat(path, identifier)
225 scm.cat(path, identifier)
226 end
226 end
227
227
228 def diff(path, rev, rev_to)
228 def diff(path, rev, rev_to)
229 scm.diff(path, rev, rev_to)
229 scm.diff(path, rev, rev_to)
230 end
230 end
231
231
232 def diff_format_revisions(cs, cs_to, sep=':')
232 def diff_format_revisions(cs, cs_to, sep=':')
233 text = ""
233 text = ""
234 text << cs_to.format_identifier + sep if cs_to
234 text << cs_to.format_identifier + sep if cs_to
235 text << cs.format_identifier if cs
235 text << cs.format_identifier if cs
236 text
236 text
237 end
237 end
238
238
239 # Returns a path relative to the url of the repository
239 # Returns a path relative to the url of the repository
240 def relative_path(path)
240 def relative_path(path)
241 path
241 path
242 end
242 end
243
243
244 # Finds and returns a revision with a number or the beginning of a hash
244 # Finds and returns a revision with a number or the beginning of a hash
245 def find_changeset_by_name(name)
245 def find_changeset_by_name(name)
246 return nil if name.blank?
246 return nil if name.blank?
247 s = name.to_s
247 s = name.to_s
248 if s.match(/^\d*$/)
248 if s.match(/^\d*$/)
249 changesets.where("revision = ?", s).first
249 changesets.where("revision = ?", s).first
250 else
250 else
251 changesets.where("revision LIKE ?", s + '%').first
251 changesets.where("revision LIKE ?", s + '%').first
252 end
252 end
253 end
253 end
254
254
255 def latest_changeset
255 def latest_changeset
256 @latest_changeset ||= changesets.first
256 @latest_changeset ||= changesets.first
257 end
257 end
258
258
259 # Returns the latest changesets for +path+
259 # Returns the latest changesets for +path+
260 # Default behaviour is to search in cached changesets
260 # Default behaviour is to search in cached changesets
261 def latest_changesets(path, rev, limit=10)
261 def latest_changesets(path, rev, limit=10)
262 if path.blank?
262 if path.blank?
263 changesets.
263 changesets.
264 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
264 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
265 limit(limit).
265 limit(limit).
266 preload(:user).
266 preload(:user).
267 all
267 all
268 else
268 else
269 filechanges.
269 filechanges.
270 where("path = ?", path.with_leading_slash).
270 where("path = ?", path.with_leading_slash).
271 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
271 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
272 limit(limit).
272 limit(limit).
273 preload(:changeset => :user).
273 preload(:changeset => :user).
274 collect(&:changeset)
274 collect(&:changeset)
275 end
275 end
276 end
276 end
277
277
278 def scan_changesets_for_issue_ids
278 def scan_changesets_for_issue_ids
279 self.changesets.each(&:scan_comment_for_issue_ids)
279 self.changesets.each(&:scan_comment_for_issue_ids)
280 end
280 end
281
281
282 # Returns an array of committers usernames and associated user_id
282 # Returns an array of committers usernames and associated user_id
283 def committers
283 def committers
284 @committers ||= Changeset.connection.select_rows(
284 @committers ||= Changeset.connection.select_rows(
285 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
285 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
286 end
286 end
287
287
288 # Maps committers username to a user ids
288 # Maps committers username to a user ids
289 def committer_ids=(h)
289 def committer_ids=(h)
290 if h.is_a?(Hash)
290 if h.is_a?(Hash)
291 committers.each do |committer, user_id|
291 committers.each do |committer, user_id|
292 new_user_id = h[committer]
292 new_user_id = h[committer]
293 if new_user_id && (new_user_id.to_i != user_id.to_i)
293 if new_user_id && (new_user_id.to_i != user_id.to_i)
294 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
294 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
295 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
295 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
296 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
296 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
297 end
297 end
298 end
298 end
299 @committers = nil
299 @committers = nil
300 @found_committer_users = nil
300 @found_committer_users = nil
301 true
301 true
302 else
302 else
303 false
303 false
304 end
304 end
305 end
305 end
306
306
307 # Returns the Redmine User corresponding to the given +committer+
307 # Returns the Redmine User corresponding to the given +committer+
308 # It will return nil if the committer is not yet mapped and if no User
308 # It will return nil if the committer is not yet mapped and if no User
309 # with the same username or email was found
309 # with the same username or email was found
310 def find_committer_user(committer)
310 def find_committer_user(committer)
311 unless committer.blank?
311 unless committer.blank?
312 @found_committer_users ||= {}
312 @found_committer_users ||= {}
313 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
313 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
314
314
315 user = nil
315 user = nil
316 c = changesets.where(:committer => committer).includes(:user).first
316 c = changesets.where(:committer => committer).includes(:user).first
317 if c && c.user
317 if c && c.user
318 user = c.user
318 user = c.user
319 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
319 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
320 username, email = $1.strip, $3
320 username, email = $1.strip, $3
321 u = User.find_by_login(username)
321 u = User.find_by_login(username)
322 u ||= User.find_by_mail(email) unless email.blank?
322 u ||= User.find_by_mail(email) unless email.blank?
323 user = u
323 user = u
324 end
324 end
325 @found_committer_users[committer] = user
325 @found_committer_users[committer] = user
326 user
326 user
327 end
327 end
328 end
328 end
329
329
330 def repo_log_encoding
330 def repo_log_encoding
331 encoding = log_encoding.to_s.strip
331 encoding = log_encoding.to_s.strip
332 encoding.blank? ? 'UTF-8' : encoding
332 encoding.blank? ? 'UTF-8' : encoding
333 end
333 end
334
334
335 # Fetches new changesets for all repositories of active projects
335 # Fetches new changesets for all repositories of active projects
336 # Can be called periodically by an external script
336 # Can be called periodically by an external script
337 # eg. ruby script/runner "Repository.fetch_changesets"
337 # eg. ruby script/runner "Repository.fetch_changesets"
338 def self.fetch_changesets
338 def self.fetch_changesets
339 Project.active.has_module(:repository).all.each do |project|
339 Project.active.has_module(:repository).all.each do |project|
340 project.repositories.each do |repository|
340 project.repositories.each do |repository|
341 begin
341 begin
342 repository.fetch_changesets
342 repository.fetch_changesets
343 rescue Redmine::Scm::Adapters::CommandFailed => e
343 rescue Redmine::Scm::Adapters::CommandFailed => e
344 logger.error "scm: error during fetching changesets: #{e.message}"
344 logger.error "scm: error during fetching changesets: #{e.message}"
345 end
345 end
346 end
346 end
347 end
347 end
348 end
348 end
349
349
350 # scan changeset comments to find related and fixed issues for all repositories
350 # scan changeset comments to find related and fixed issues for all repositories
351 def self.scan_changesets_for_issue_ids
351 def self.scan_changesets_for_issue_ids
352 all.each(&:scan_changesets_for_issue_ids)
352 all.each(&:scan_changesets_for_issue_ids)
353 end
353 end
354
354
355 def self.scm_name
355 def self.scm_name
356 'Abstract'
356 'Abstract'
357 end
357 end
358
358
359 def self.available_scm
359 def self.available_scm
360 subclasses.collect {|klass| [klass.scm_name, klass.name]}
360 subclasses.collect {|klass| [klass.scm_name, klass.name]}
361 end
361 end
362
362
363 def self.factory(klass_name, *args)
363 def self.factory(klass_name, *args)
364 klass = "Repository::#{klass_name}".constantize
364 klass = "Repository::#{klass_name}".constantize
365 klass.new(*args)
365 klass.new(*args)
366 rescue
366 rescue
367 nil
367 nil
368 end
368 end
369
369
370 def self.scm_adapter_class
370 def self.scm_adapter_class
371 nil
371 nil
372 end
372 end
373
373
374 def self.scm_command
374 def self.scm_command
375 ret = ""
375 ret = ""
376 begin
376 begin
377 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
377 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
378 rescue Exception => e
378 rescue Exception => e
379 logger.error "scm: error during get command: #{e.message}"
379 logger.error "scm: error during get command: #{e.message}"
380 end
380 end
381 ret
381 ret
382 end
382 end
383
383
384 def self.scm_version_string
384 def self.scm_version_string
385 ret = ""
385 ret = ""
386 begin
386 begin
387 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
387 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
388 rescue Exception => e
388 rescue Exception => e
389 logger.error "scm: error during get version string: #{e.message}"
389 logger.error "scm: error during get version string: #{e.message}"
390 end
390 end
391 ret
391 ret
392 end
392 end
393
393
394 def self.scm_available
394 def self.scm_available
395 ret = false
395 ret = false
396 begin
396 begin
397 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
397 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
398 rescue Exception => e
398 rescue Exception => e
399 logger.error "scm: error during get scm available: #{e.message}"
399 logger.error "scm: error during get scm available: #{e.message}"
400 end
400 end
401 ret
401 ret
402 end
402 end
403
403
404 def set_as_default?
404 def set_as_default?
405 new_record? && project && Repository.where(:project_id => project.id).empty?
405 new_record? && project && Repository.where(:project_id => project.id).empty?
406 end
406 end
407
407
408 # Returns a hash with statistics by author in the following form:
408 # Returns a hash with statistics by author in the following form:
409 # {
409 # {
410 # "John Smith" => { :commits => 45, :changes => 324 },
410 # "John Smith" => { :commits => 45, :changes => 324 },
411 # "Bob" => { ... }
411 # "Bob" => { ... }
412 # }
412 # }
413 #
413 #
414 # Notes:
414 # Notes:
415 # - this hash honnors the users mapping defined for the repository
415 # - this hash honnors the users mapping defined for the repository
416 def stats_by_author
416 def stats_by_author
417 commits = Changeset.where("repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
417 commits = Changeset.where("repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
418
418
419 #TODO: restore ordering ; this line probably never worked
419 #TODO: restore ordering ; this line probably never worked
420 #commits.to_a.sort! {|x, y| x.last <=> y.last}
420 #commits.to_a.sort! {|x, y| x.last <=> y.last}
421
421
422 changes = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
422 changes = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
423
423
424 user_ids = changesets.map(&:user_id).compact.uniq
424 user_ids = changesets.map(&:user_id).compact.uniq
425 authors_names = User.where(:id => user_ids).inject({}) do |memo, user|
425 authors_names = User.where(:id => user_ids).inject({}) do |memo, user|
426 memo[user.id] = user.to_s
426 memo[user.id] = user.to_s
427 memo
427 memo
428 end
428 end
429
429
430 (commits + changes).inject({}) do |hash, element|
430 (commits + changes).inject({}) do |hash, element|
431 mapped_name = element.committer
431 mapped_name = element.committer
432 if username = authors_names[element.user_id.to_i]
432 if username = authors_names[element.user_id.to_i]
433 mapped_name = username
433 mapped_name = username
434 end
434 end
435 hash[mapped_name] ||= { :commits_count => 0, :changes_count => 0 }
435 hash[mapped_name] ||= { :commits_count => 0, :changes_count => 0 }
436 if element.is_a?(Changeset)
436 if element.is_a?(Changeset)
437 hash[mapped_name][:commits_count] += element.count.to_i
437 hash[mapped_name][:commits_count] += element.count.to_i
438 else
438 else
439 hash[mapped_name][:changes_count] += element.count.to_i
439 hash[mapped_name][:changes_count] += element.count.to_i
440 end
440 end
441 hash
441 hash
442 end
442 end
443 end
443 end
444
444
445 # Returns a scope of changesets that come from the same commit as the given changeset
446 # in different repositories that point to the same backend
447 def same_commits_in_scope(scope, changeset)
448 scope = scope.joins(:repository).where(:repositories => {:url => url, :root_url => root_url, :type => type})
449 if changeset.scmid.present?
450 scope = scope.where(:scmid => changeset.scmid)
451 else
452 scope = scope.where(:revision => changeset.revision)
453 end
454 scope
455 end
456
445 protected
457 protected
446
458
447 def check_default
459 def check_default
448 if !is_default? && set_as_default?
460 if !is_default? && set_as_default?
449 self.is_default = true
461 self.is_default = true
450 end
462 end
451 if is_default? && is_default_changed?
463 if is_default? && is_default_changed?
452 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
464 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
453 end
465 end
454 end
466 end
455
467
456 def load_entries_changesets(entries)
468 def load_entries_changesets(entries)
457 if entries
469 if entries
458 entries.each do |entry|
470 entries.each do |entry|
459 if entry.lastrev && entry.lastrev.identifier
471 if entry.lastrev && entry.lastrev.identifier
460 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
472 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
461 end
473 end
462 end
474 end
463 end
475 end
464 end
476 end
465
477
466 private
478 private
467
479
468 # Deletes repository data
480 # Deletes repository data
469 def clear_changesets
481 def clear_changesets
470 cs = Changeset.table_name
482 cs = Changeset.table_name
471 ch = Change.table_name
483 ch = Change.table_name
472 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
484 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
473 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
485 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
474
486
475 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
487 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
476 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
488 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
477 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
489 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
478 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
490 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
479 clear_extra_info_of_changesets
491 clear_extra_info_of_changesets
480 end
492 end
481
493
482 def clear_extra_info_of_changesets
494 def clear_extra_info_of_changesets
483 end
495 end
484 end
496 end
@@ -1,555 +1,566
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class ChangesetTest < ActiveSupport::TestCase
22 class ChangesetTest < ActiveSupport::TestCase
23 fixtures :projects, :repositories,
23 fixtures :projects, :repositories,
24 :issues, :issue_statuses, :issue_categories,
24 :issues, :issue_statuses, :issue_categories,
25 :changesets, :changes,
25 :changesets, :changes,
26 :enumerations,
26 :enumerations,
27 :custom_fields, :custom_values,
27 :custom_fields, :custom_values,
28 :users, :members, :member_roles,
28 :users, :members, :member_roles,
29 :trackers, :projects_trackers,
29 :trackers, :projects_trackers,
30 :enabled_modules, :roles
30 :enabled_modules, :roles
31
31
32 def test_ref_keywords_any
32 def test_ref_keywords_any
33 ActionMailer::Base.deliveries.clear
33 ActionMailer::Base.deliveries.clear
34 Setting.commit_ref_keywords = '*'
34 Setting.commit_ref_keywords = '*'
35 Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', 'status_id' => '5', 'done_ratio' => '90'}]
35 Setting.commit_update_keywords = [{'keywords' => 'fixes , closes', 'status_id' => '5', 'done_ratio' => '90'}]
36
36
37 c = Changeset.new(:repository => Project.find(1).repository,
37 c = Changeset.new(:repository => Project.find(1).repository,
38 :committed_on => Time.now,
38 :committed_on => Time.now,
39 :comments => 'New commit (#2). Fixes #1',
39 :comments => 'New commit (#2). Fixes #1',
40 :revision => '12345')
40 :revision => '12345')
41 assert c.save
41 assert c.save
42 assert_equal [1, 2], c.issue_ids.sort
42 assert_equal [1, 2], c.issue_ids.sort
43 fixed = Issue.find(1)
43 fixed = Issue.find(1)
44 assert fixed.closed?
44 assert fixed.closed?
45 assert_equal 90, fixed.done_ratio
45 assert_equal 90, fixed.done_ratio
46 assert_equal 1, ActionMailer::Base.deliveries.size
46 assert_equal 1, ActionMailer::Base.deliveries.size
47 end
47 end
48
48
49 def test_ref_keywords
49 def test_ref_keywords
50 Setting.commit_ref_keywords = 'refs'
50 Setting.commit_ref_keywords = 'refs'
51 Setting.commit_update_keywords = ''
51 Setting.commit_update_keywords = ''
52 c = Changeset.new(:repository => Project.find(1).repository,
52 c = Changeset.new(:repository => Project.find(1).repository,
53 :committed_on => Time.now,
53 :committed_on => Time.now,
54 :comments => 'Ignores #2. Refs #1',
54 :comments => 'Ignores #2. Refs #1',
55 :revision => '12345')
55 :revision => '12345')
56 assert c.save
56 assert c.save
57 assert_equal [1], c.issue_ids.sort
57 assert_equal [1], c.issue_ids.sort
58 end
58 end
59
59
60 def test_ref_keywords_any_only
60 def test_ref_keywords_any_only
61 Setting.commit_ref_keywords = '*'
61 Setting.commit_ref_keywords = '*'
62 Setting.commit_update_keywords = ''
62 Setting.commit_update_keywords = ''
63 c = Changeset.new(:repository => Project.find(1).repository,
63 c = Changeset.new(:repository => Project.find(1).repository,
64 :committed_on => Time.now,
64 :committed_on => Time.now,
65 :comments => 'Ignores #2. Refs #1',
65 :comments => 'Ignores #2. Refs #1',
66 :revision => '12345')
66 :revision => '12345')
67 assert c.save
67 assert c.save
68 assert_equal [1, 2], c.issue_ids.sort
68 assert_equal [1, 2], c.issue_ids.sort
69 end
69 end
70
70
71 def test_ref_keywords_any_with_timelog
71 def test_ref_keywords_any_with_timelog
72 Setting.commit_ref_keywords = '*'
72 Setting.commit_ref_keywords = '*'
73 Setting.commit_logtime_enabled = '1'
73 Setting.commit_logtime_enabled = '1'
74
74
75 {
75 {
76 '2' => 2.0,
76 '2' => 2.0,
77 '2h' => 2.0,
77 '2h' => 2.0,
78 '2hours' => 2.0,
78 '2hours' => 2.0,
79 '15m' => 0.25,
79 '15m' => 0.25,
80 '15min' => 0.25,
80 '15min' => 0.25,
81 '3h15' => 3.25,
81 '3h15' => 3.25,
82 '3h15m' => 3.25,
82 '3h15m' => 3.25,
83 '3h15min' => 3.25,
83 '3h15min' => 3.25,
84 '3:15' => 3.25,
84 '3:15' => 3.25,
85 '3.25' => 3.25,
85 '3.25' => 3.25,
86 '3.25h' => 3.25,
86 '3.25h' => 3.25,
87 '3,25' => 3.25,
87 '3,25' => 3.25,
88 '3,25h' => 3.25,
88 '3,25h' => 3.25,
89 }.each do |syntax, expected_hours|
89 }.each do |syntax, expected_hours|
90 c = Changeset.new(:repository => Project.find(1).repository,
90 c = Changeset.new(:repository => Project.find(1).repository,
91 :committed_on => 24.hours.ago,
91 :committed_on => 24.hours.ago,
92 :comments => "Worked on this issue #1 @#{syntax}",
92 :comments => "Worked on this issue #1 @#{syntax}",
93 :revision => '520',
93 :revision => '520',
94 :user => User.find(2))
94 :user => User.find(2))
95 assert_difference 'TimeEntry.count' do
95 assert_difference 'TimeEntry.count' do
96 c.scan_comment_for_issue_ids
96 c.scan_comment_for_issue_ids
97 end
97 end
98 assert_equal [1], c.issue_ids.sort
98 assert_equal [1], c.issue_ids.sort
99
99
100 time = TimeEntry.order('id desc').first
100 time = TimeEntry.order('id desc').first
101 assert_equal 1, time.issue_id
101 assert_equal 1, time.issue_id
102 assert_equal 1, time.project_id
102 assert_equal 1, time.project_id
103 assert_equal 2, time.user_id
103 assert_equal 2, time.user_id
104 assert_equal expected_hours, time.hours,
104 assert_equal expected_hours, time.hours,
105 "@#{syntax} should be logged as #{expected_hours} hours but was #{time.hours}"
105 "@#{syntax} should be logged as #{expected_hours} hours but was #{time.hours}"
106 assert_equal Date.yesterday, time.spent_on
106 assert_equal Date.yesterday, time.spent_on
107 assert time.activity.is_default?
107 assert time.activity.is_default?
108 assert time.comments.include?('r520'),
108 assert time.comments.include?('r520'),
109 "r520 was expected in time_entry comments: #{time.comments}"
109 "r520 was expected in time_entry comments: #{time.comments}"
110 end
110 end
111 end
111 end
112
112
113 def test_ref_keywords_closing_with_timelog
113 def test_ref_keywords_closing_with_timelog
114 Setting.commit_ref_keywords = '*'
114 Setting.commit_ref_keywords = '*'
115 Setting.commit_update_keywords = [{'keywords' => 'fixes , closes',
115 Setting.commit_update_keywords = [{'keywords' => 'fixes , closes',
116 'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s}]
116 'status_id' => IssueStatus.where(:is_closed => true).first.id.to_s}]
117 Setting.commit_logtime_enabled = '1'
117 Setting.commit_logtime_enabled = '1'
118
118
119 c = Changeset.new(:repository => Project.find(1).repository,
119 c = Changeset.new(:repository => Project.find(1).repository,
120 :committed_on => Time.now,
120 :committed_on => Time.now,
121 :comments => 'This is a comment. Fixes #1 @4.5, #2 @1',
121 :comments => 'This is a comment. Fixes #1 @4.5, #2 @1',
122 :user => User.find(2))
122 :user => User.find(2))
123 assert_difference 'TimeEntry.count', 2 do
123 assert_difference 'TimeEntry.count', 2 do
124 c.scan_comment_for_issue_ids
124 c.scan_comment_for_issue_ids
125 end
125 end
126
126
127 assert_equal [1, 2], c.issue_ids.sort
127 assert_equal [1, 2], c.issue_ids.sort
128 assert Issue.find(1).closed?
128 assert Issue.find(1).closed?
129 assert Issue.find(2).closed?
129 assert Issue.find(2).closed?
130
130
131 times = TimeEntry.order('id desc').limit(2)
131 times = TimeEntry.order('id desc').limit(2)
132 assert_equal [1, 2], times.collect(&:issue_id).sort
132 assert_equal [1, 2], times.collect(&:issue_id).sort
133 end
133 end
134
134
135 def test_ref_keywords_any_line_start
135 def test_ref_keywords_any_line_start
136 Setting.commit_ref_keywords = '*'
136 Setting.commit_ref_keywords = '*'
137 c = Changeset.new(:repository => Project.find(1).repository,
137 c = Changeset.new(:repository => Project.find(1).repository,
138 :committed_on => Time.now,
138 :committed_on => Time.now,
139 :comments => '#1 is the reason of this commit',
139 :comments => '#1 is the reason of this commit',
140 :revision => '12345')
140 :revision => '12345')
141 assert c.save
141 assert c.save
142 assert_equal [1], c.issue_ids.sort
142 assert_equal [1], c.issue_ids.sort
143 end
143 end
144
144
145 def test_ref_keywords_allow_brackets_around_a_issue_number
145 def test_ref_keywords_allow_brackets_around_a_issue_number
146 Setting.commit_ref_keywords = '*'
146 Setting.commit_ref_keywords = '*'
147 c = Changeset.new(:repository => Project.find(1).repository,
147 c = Changeset.new(:repository => Project.find(1).repository,
148 :committed_on => Time.now,
148 :committed_on => Time.now,
149 :comments => '[#1] Worked on this issue',
149 :comments => '[#1] Worked on this issue',
150 :revision => '12345')
150 :revision => '12345')
151 assert c.save
151 assert c.save
152 assert_equal [1], c.issue_ids.sort
152 assert_equal [1], c.issue_ids.sort
153 end
153 end
154
154
155 def test_ref_keywords_allow_brackets_around_multiple_issue_numbers
155 def test_ref_keywords_allow_brackets_around_multiple_issue_numbers
156 Setting.commit_ref_keywords = '*'
156 Setting.commit_ref_keywords = '*'
157 c = Changeset.new(:repository => Project.find(1).repository,
157 c = Changeset.new(:repository => Project.find(1).repository,
158 :committed_on => Time.now,
158 :committed_on => Time.now,
159 :comments => '[#1 #2, #3] Worked on these',
159 :comments => '[#1 #2, #3] Worked on these',
160 :revision => '12345')
160 :revision => '12345')
161 assert c.save
161 assert c.save
162 assert_equal [1,2,3], c.issue_ids.sort
162 assert_equal [1,2,3], c.issue_ids.sort
163 end
163 end
164
164
165 def test_update_keywords_with_multiple_rules
165 def test_update_keywords_with_multiple_rules
166 with_settings :commit_update_keywords => [
166 with_settings :commit_update_keywords => [
167 {'keywords' => 'fixes, closes', 'status_id' => '5'},
167 {'keywords' => 'fixes, closes', 'status_id' => '5'},
168 {'keywords' => 'resolves', 'status_id' => '3'}
168 {'keywords' => 'resolves', 'status_id' => '3'}
169 ] do
169 ] do
170
170
171 issue1 = Issue.generate!
171 issue1 = Issue.generate!
172 issue2 = Issue.generate!
172 issue2 = Issue.generate!
173 Changeset.generate!(:comments => "Closes ##{issue1.id}\nResolves ##{issue2.id}")
173 Changeset.generate!(:comments => "Closes ##{issue1.id}\nResolves ##{issue2.id}")
174 assert_equal 5, issue1.reload.status_id
174 assert_equal 5, issue1.reload.status_id
175 assert_equal 3, issue2.reload.status_id
175 assert_equal 3, issue2.reload.status_id
176 end
176 end
177 end
177 end
178
178
179 def test_update_keywords_with_multiple_rules_should_match_tracker
179 def test_update_keywords_with_multiple_rules_should_match_tracker
180 with_settings :commit_update_keywords => [
180 with_settings :commit_update_keywords => [
181 {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
181 {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
182 {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => ''}
182 {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => ''}
183 ] do
183 ] do
184
184
185 issue1 = Issue.generate!(:tracker_id => 2)
185 issue1 = Issue.generate!(:tracker_id => 2)
186 issue2 = Issue.generate!
186 issue2 = Issue.generate!
187 Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
187 Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
188 assert_equal 5, issue1.reload.status_id
188 assert_equal 5, issue1.reload.status_id
189 assert_equal 3, issue2.reload.status_id
189 assert_equal 3, issue2.reload.status_id
190 end
190 end
191 end
191 end
192
192
193 def test_update_keywords_with_multiple_rules_and_no_match
193 def test_update_keywords_with_multiple_rules_and_no_match
194 with_settings :commit_update_keywords => [
194 with_settings :commit_update_keywords => [
195 {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
195 {'keywords' => 'fixes', 'status_id' => '5', 'if_tracker_id' => '2'},
196 {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => '3'}
196 {'keywords' => 'fixes', 'status_id' => '3', 'if_tracker_id' => '3'}
197 ] do
197 ] do
198
198
199 issue1 = Issue.generate!(:tracker_id => 2)
199 issue1 = Issue.generate!(:tracker_id => 2)
200 issue2 = Issue.generate!
200 issue2 = Issue.generate!
201 Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
201 Changeset.generate!(:comments => "Fixes ##{issue1.id}, ##{issue2.id}")
202 assert_equal 5, issue1.reload.status_id
202 assert_equal 5, issue1.reload.status_id
203 assert_equal 1, issue2.reload.status_id # no updates
203 assert_equal 1, issue2.reload.status_id # no updates
204 end
204 end
205 end
205 end
206
206
207 def test_commit_referencing_a_subproject_issue
207 def test_commit_referencing_a_subproject_issue
208 c = Changeset.new(:repository => Project.find(1).repository,
208 c = Changeset.new(:repository => Project.find(1).repository,
209 :committed_on => Time.now,
209 :committed_on => Time.now,
210 :comments => 'refs #5, a subproject issue',
210 :comments => 'refs #5, a subproject issue',
211 :revision => '12345')
211 :revision => '12345')
212 assert c.save
212 assert c.save
213 assert_equal [5], c.issue_ids.sort
213 assert_equal [5], c.issue_ids.sort
214 assert c.issues.first.project != c.project
214 assert c.issues.first.project != c.project
215 end
215 end
216
216
217 def test_commit_closing_a_subproject_issue
217 def test_commit_closing_a_subproject_issue
218 with_settings :commit_update_keywords => [{'keywords' => 'closes', 'status_id' => '5'}],
218 with_settings :commit_update_keywords => [{'keywords' => 'closes', 'status_id' => '5'}],
219 :default_language => 'en' do
219 :default_language => 'en' do
220 issue = Issue.find(5)
220 issue = Issue.find(5)
221 assert !issue.closed?
221 assert !issue.closed?
222 assert_difference 'Journal.count' do
222 assert_difference 'Journal.count' do
223 c = Changeset.new(:repository => Project.find(1).repository,
223 c = Changeset.new(:repository => Project.find(1).repository,
224 :committed_on => Time.now,
224 :committed_on => Time.now,
225 :comments => 'closes #5, a subproject issue',
225 :comments => 'closes #5, a subproject issue',
226 :revision => '12345')
226 :revision => '12345')
227 assert c.save
227 assert c.save
228 end
228 end
229 assert issue.reload.closed?
229 assert issue.reload.closed?
230 journal = Journal.order('id DESC').first
230 journal = Journal.order('id DESC').first
231 assert_equal issue, journal.issue
231 assert_equal issue, journal.issue
232 assert_include "Applied in changeset ecookbook:r12345.", journal.notes
232 assert_include "Applied in changeset ecookbook:r12345.", journal.notes
233 end
233 end
234 end
234 end
235
235
236 def test_commit_referencing_a_parent_project_issue
236 def test_commit_referencing_a_parent_project_issue
237 # repository of child project
237 # repository of child project
238 r = Repository::Subversion.create!(
238 r = Repository::Subversion.create!(
239 :project => Project.find(3),
239 :project => Project.find(3),
240 :url => 'svn://localhost/test')
240 :url => 'svn://localhost/test')
241 c = Changeset.new(:repository => r,
241 c = Changeset.new(:repository => r,
242 :committed_on => Time.now,
242 :committed_on => Time.now,
243 :comments => 'refs #2, an issue of a parent project',
243 :comments => 'refs #2, an issue of a parent project',
244 :revision => '12345')
244 :revision => '12345')
245 assert c.save
245 assert c.save
246 assert_equal [2], c.issue_ids.sort
246 assert_equal [2], c.issue_ids.sort
247 assert c.issues.first.project != c.project
247 assert c.issues.first.project != c.project
248 end
248 end
249
249
250 def test_commit_referencing_a_project_with_commit_cross_project_ref_disabled
250 def test_commit_referencing_a_project_with_commit_cross_project_ref_disabled
251 r = Repository::Subversion.create!(
251 r = Repository::Subversion.create!(
252 :project => Project.find(3),
252 :project => Project.find(3),
253 :url => 'svn://localhost/test')
253 :url => 'svn://localhost/test')
254 with_settings :commit_cross_project_ref => '0' do
254 with_settings :commit_cross_project_ref => '0' do
255 c = Changeset.new(:repository => r,
255 c = Changeset.new(:repository => r,
256 :committed_on => Time.now,
256 :committed_on => Time.now,
257 :comments => 'refs #4, an issue of a different project',
257 :comments => 'refs #4, an issue of a different project',
258 :revision => '12345')
258 :revision => '12345')
259 assert c.save
259 assert c.save
260 assert_equal [], c.issue_ids
260 assert_equal [], c.issue_ids
261 end
261 end
262 end
262 end
263
263
264 def test_commit_referencing_a_project_with_commit_cross_project_ref_enabled
264 def test_commit_referencing_a_project_with_commit_cross_project_ref_enabled
265 r = Repository::Subversion.create!(
265 r = Repository::Subversion.create!(
266 :project => Project.find(3),
266 :project => Project.find(3),
267 :url => 'svn://localhost/test')
267 :url => 'svn://localhost/test')
268 with_settings :commit_cross_project_ref => '1' do
268 with_settings :commit_cross_project_ref => '1' do
269 c = Changeset.new(:repository => r,
269 c = Changeset.new(:repository => r,
270 :committed_on => Time.now,
270 :committed_on => Time.now,
271 :comments => 'refs #4, an issue of a different project',
271 :comments => 'refs #4, an issue of a different project',
272 :revision => '12345')
272 :revision => '12345')
273 assert c.save
273 assert c.save
274 assert_equal [4], c.issue_ids
274 assert_equal [4], c.issue_ids
275 end
275 end
276 end
276 end
277
277
278 def test_old_commits_should_not_update_issues_nor_log_time
278 def test_old_commits_should_not_update_issues_nor_log_time
279 Setting.commit_ref_keywords = '*'
279 Setting.commit_ref_keywords = '*'
280 Setting.commit_update_keywords = {'fixes , closes' => {'status_id' => '5', 'done_ratio' => '90'}}
280 Setting.commit_update_keywords = {'fixes , closes' => {'status_id' => '5', 'done_ratio' => '90'}}
281 Setting.commit_logtime_enabled = '1'
281 Setting.commit_logtime_enabled = '1'
282
282
283 repository = Project.find(1).repository
283 repository = Project.find(1).repository
284 repository.created_on = Time.now
284 repository.created_on = Time.now
285 repository.save!
285 repository.save!
286
286
287 c = Changeset.new(:repository => repository,
287 c = Changeset.new(:repository => repository,
288 :committed_on => 1.month.ago,
288 :committed_on => 1.month.ago,
289 :comments => 'New commit (#2). Fixes #1 @1h',
289 :comments => 'New commit (#2). Fixes #1 @1h',
290 :revision => '12345')
290 :revision => '12345')
291 assert_no_difference 'TimeEntry.count' do
291 assert_no_difference 'TimeEntry.count' do
292 assert c.save
292 assert c.save
293 end
293 end
294 assert_equal [1, 2], c.issue_ids.sort
294 assert_equal [1, 2], c.issue_ids.sort
295 issue = Issue.find(1)
295 issue = Issue.find(1)
296 assert_equal 1, issue.status_id
296 assert_equal 1, issue.status_id
297 assert_equal 0, issue.done_ratio
297 assert_equal 0, issue.done_ratio
298 end
298 end
299
299
300 def test_2_repositories_with_same_backend_should_not_link_issue_multiple_times
301 Setting.commit_ref_keywords = '*'
302 r1 = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///svn1')
303 r2 = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn2', :url => 'file:///svn1')
304 now = Time.now
305 assert_difference 'Issue.find(1).changesets.count' do
306 c1 = Changeset.create!(:repository => r1, :committed_on => now, :comments => 'Fixes #1', :revision => '12345')
307 c1 = Changeset.create!(:repository => r2, :committed_on => now, :comments => 'Fixes #1', :revision => '12345')
308 end
309 end
310
300 def test_text_tag_revision
311 def test_text_tag_revision
301 c = Changeset.new(:revision => '520')
312 c = Changeset.new(:revision => '520')
302 assert_equal 'r520', c.text_tag
313 assert_equal 'r520', c.text_tag
303 end
314 end
304
315
305 def test_text_tag_revision_with_same_project
316 def test_text_tag_revision_with_same_project
306 c = Changeset.new(:revision => '520', :repository => Project.find(1).repository)
317 c = Changeset.new(:revision => '520', :repository => Project.find(1).repository)
307 assert_equal 'r520', c.text_tag(Project.find(1))
318 assert_equal 'r520', c.text_tag(Project.find(1))
308 end
319 end
309
320
310 def test_text_tag_revision_with_different_project
321 def test_text_tag_revision_with_different_project
311 c = Changeset.new(:revision => '520', :repository => Project.find(1).repository)
322 c = Changeset.new(:revision => '520', :repository => Project.find(1).repository)
312 assert_equal 'ecookbook:r520', c.text_tag(Project.find(2))
323 assert_equal 'ecookbook:r520', c.text_tag(Project.find(2))
313 end
324 end
314
325
315 def test_text_tag_revision_with_repository_identifier
326 def test_text_tag_revision_with_repository_identifier
316 r = Repository::Subversion.create!(
327 r = Repository::Subversion.create!(
317 :project_id => 1,
328 :project_id => 1,
318 :url => 'svn://localhost/test',
329 :url => 'svn://localhost/test',
319 :identifier => 'documents')
330 :identifier => 'documents')
320 c = Changeset.new(:revision => '520', :repository => r)
331 c = Changeset.new(:revision => '520', :repository => r)
321 assert_equal 'documents|r520', c.text_tag
332 assert_equal 'documents|r520', c.text_tag
322 assert_equal 'ecookbook:documents|r520', c.text_tag(Project.find(2))
333 assert_equal 'ecookbook:documents|r520', c.text_tag(Project.find(2))
323 end
334 end
324
335
325 def test_text_tag_hash
336 def test_text_tag_hash
326 c = Changeset.new(
337 c = Changeset.new(
327 :scmid => '7234cb2750b63f47bff735edc50a1c0a433c2518',
338 :scmid => '7234cb2750b63f47bff735edc50a1c0a433c2518',
328 :revision => '7234cb2750b63f47bff735edc50a1c0a433c2518')
339 :revision => '7234cb2750b63f47bff735edc50a1c0a433c2518')
329 assert_equal 'commit:7234cb2750b63f47bff735edc50a1c0a433c2518', c.text_tag
340 assert_equal 'commit:7234cb2750b63f47bff735edc50a1c0a433c2518', c.text_tag
330 end
341 end
331
342
332 def test_text_tag_hash_with_same_project
343 def test_text_tag_hash_with_same_project
333 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository)
344 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository)
334 assert_equal 'commit:7234cb27', c.text_tag(Project.find(1))
345 assert_equal 'commit:7234cb27', c.text_tag(Project.find(1))
335 end
346 end
336
347
337 def test_text_tag_hash_with_different_project
348 def test_text_tag_hash_with_different_project
338 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository)
349 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository)
339 assert_equal 'ecookbook:commit:7234cb27', c.text_tag(Project.find(2))
350 assert_equal 'ecookbook:commit:7234cb27', c.text_tag(Project.find(2))
340 end
351 end
341
352
342 def test_text_tag_hash_all_number
353 def test_text_tag_hash_all_number
343 c = Changeset.new(:scmid => '0123456789', :revision => '0123456789')
354 c = Changeset.new(:scmid => '0123456789', :revision => '0123456789')
344 assert_equal 'commit:0123456789', c.text_tag
355 assert_equal 'commit:0123456789', c.text_tag
345 end
356 end
346
357
347 def test_text_tag_hash_with_repository_identifier
358 def test_text_tag_hash_with_repository_identifier
348 r = Repository::Subversion.new(
359 r = Repository::Subversion.new(
349 :project_id => 1,
360 :project_id => 1,
350 :url => 'svn://localhost/test',
361 :url => 'svn://localhost/test',
351 :identifier => 'documents')
362 :identifier => 'documents')
352 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => r)
363 c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => r)
353 assert_equal 'commit:documents|7234cb27', c.text_tag
364 assert_equal 'commit:documents|7234cb27', c.text_tag
354 assert_equal 'ecookbook:commit:documents|7234cb27', c.text_tag(Project.find(2))
365 assert_equal 'ecookbook:commit:documents|7234cb27', c.text_tag(Project.find(2))
355 end
366 end
356
367
357 def test_previous
368 def test_previous
358 changeset = Changeset.find_by_revision('3')
369 changeset = Changeset.find_by_revision('3')
359 assert_equal Changeset.find_by_revision('2'), changeset.previous
370 assert_equal Changeset.find_by_revision('2'), changeset.previous
360 end
371 end
361
372
362 def test_previous_nil
373 def test_previous_nil
363 changeset = Changeset.find_by_revision('1')
374 changeset = Changeset.find_by_revision('1')
364 assert_nil changeset.previous
375 assert_nil changeset.previous
365 end
376 end
366
377
367 def test_next
378 def test_next
368 changeset = Changeset.find_by_revision('2')
379 changeset = Changeset.find_by_revision('2')
369 assert_equal Changeset.find_by_revision('3'), changeset.next
380 assert_equal Changeset.find_by_revision('3'), changeset.next
370 end
381 end
371
382
372 def test_next_nil
383 def test_next_nil
373 changeset = Changeset.find_by_revision('10')
384 changeset = Changeset.find_by_revision('10')
374 assert_nil changeset.next
385 assert_nil changeset.next
375 end
386 end
376
387
377 def test_comments_should_be_converted_to_utf8
388 def test_comments_should_be_converted_to_utf8
378 proj = Project.find(3)
389 proj = Project.find(3)
379 # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
390 # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
380 str = "Texte encod\xe9 en ISO-8859-1."
391 str = "Texte encod\xe9 en ISO-8859-1."
381 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
392 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
382 r = Repository::Bazaar.create!(
393 r = Repository::Bazaar.create!(
383 :project => proj,
394 :project => proj,
384 :url => '/tmp/test/bazaar',
395 :url => '/tmp/test/bazaar',
385 :log_encoding => 'ISO-8859-1' )
396 :log_encoding => 'ISO-8859-1' )
386 assert r
397 assert r
387 c = Changeset.new(:repository => r,
398 c = Changeset.new(:repository => r,
388 :committed_on => Time.now,
399 :committed_on => Time.now,
389 :revision => '123',
400 :revision => '123',
390 :scmid => '12345',
401 :scmid => '12345',
391 :comments => str)
402 :comments => str)
392 assert( c.save )
403 assert( c.save )
393 str_utf8 = "Texte encod\xc3\xa9 en ISO-8859-1."
404 str_utf8 = "Texte encod\xc3\xa9 en ISO-8859-1."
394 str_utf8.force_encoding("UTF-8") if str_utf8.respond_to?(:force_encoding)
405 str_utf8.force_encoding("UTF-8") if str_utf8.respond_to?(:force_encoding)
395 assert_equal str_utf8, c.comments
406 assert_equal str_utf8, c.comments
396 end
407 end
397
408
398 def test_invalid_utf8_sequences_in_comments_should_be_replaced_latin1
409 def test_invalid_utf8_sequences_in_comments_should_be_replaced_latin1
399 proj = Project.find(3)
410 proj = Project.find(3)
400 # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
411 # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
401 str1 = "Texte encod\xe9 en ISO-8859-1."
412 str1 = "Texte encod\xe9 en ISO-8859-1."
402 str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
413 str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
403 str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
414 str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
404 str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
415 str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
405 r = Repository::Bazaar.create!(
416 r = Repository::Bazaar.create!(
406 :project => proj,
417 :project => proj,
407 :url => '/tmp/test/bazaar',
418 :url => '/tmp/test/bazaar',
408 :log_encoding => 'UTF-8' )
419 :log_encoding => 'UTF-8' )
409 assert r
420 assert r
410 c = Changeset.new(:repository => r,
421 c = Changeset.new(:repository => r,
411 :committed_on => Time.now,
422 :committed_on => Time.now,
412 :revision => '123',
423 :revision => '123',
413 :scmid => '12345',
424 :scmid => '12345',
414 :comments => str1,
425 :comments => str1,
415 :committer => str2)
426 :committer => str2)
416 assert( c.save )
427 assert( c.save )
417 assert_equal "Texte encod? en ISO-8859-1.", c.comments
428 assert_equal "Texte encod? en ISO-8859-1.", c.comments
418 assert_equal "?a?b?c?d?e test", c.committer
429 assert_equal "?a?b?c?d?e test", c.committer
419 end
430 end
420
431
421 def test_invalid_utf8_sequences_in_comments_should_be_replaced_ja_jis
432 def test_invalid_utf8_sequences_in_comments_should_be_replaced_ja_jis
422 proj = Project.find(3)
433 proj = Project.find(3)
423 str = "test\xb5\xfetest\xb5\xfe"
434 str = "test\xb5\xfetest\xb5\xfe"
424 if str.respond_to?(:force_encoding)
435 if str.respond_to?(:force_encoding)
425 str.force_encoding('ASCII-8BIT')
436 str.force_encoding('ASCII-8BIT')
426 end
437 end
427 r = Repository::Bazaar.create!(
438 r = Repository::Bazaar.create!(
428 :project => proj,
439 :project => proj,
429 :url => '/tmp/test/bazaar',
440 :url => '/tmp/test/bazaar',
430 :log_encoding => 'ISO-2022-JP' )
441 :log_encoding => 'ISO-2022-JP' )
431 assert r
442 assert r
432 c = Changeset.new(:repository => r,
443 c = Changeset.new(:repository => r,
433 :committed_on => Time.now,
444 :committed_on => Time.now,
434 :revision => '123',
445 :revision => '123',
435 :scmid => '12345',
446 :scmid => '12345',
436 :comments => str)
447 :comments => str)
437 assert( c.save )
448 assert( c.save )
438 assert_equal "test??test??", c.comments
449 assert_equal "test??test??", c.comments
439 end
450 end
440
451
441 def test_comments_should_be_converted_all_latin1_to_utf8
452 def test_comments_should_be_converted_all_latin1_to_utf8
442 s1 = "\xC2\x80"
453 s1 = "\xC2\x80"
443 s2 = "\xc3\x82\xc2\x80"
454 s2 = "\xc3\x82\xc2\x80"
444 s4 = s2.dup
455 s4 = s2.dup
445 if s1.respond_to?(:force_encoding)
456 if s1.respond_to?(:force_encoding)
446 s3 = s1.dup
457 s3 = s1.dup
447 s1.force_encoding('ASCII-8BIT')
458 s1.force_encoding('ASCII-8BIT')
448 s2.force_encoding('ASCII-8BIT')
459 s2.force_encoding('ASCII-8BIT')
449 s3.force_encoding('ISO-8859-1')
460 s3.force_encoding('ISO-8859-1')
450 s4.force_encoding('UTF-8')
461 s4.force_encoding('UTF-8')
451 assert_equal s3.encode('UTF-8'), s4
462 assert_equal s3.encode('UTF-8'), s4
452 end
463 end
453 proj = Project.find(3)
464 proj = Project.find(3)
454 r = Repository::Bazaar.create!(
465 r = Repository::Bazaar.create!(
455 :project => proj,
466 :project => proj,
456 :url => '/tmp/test/bazaar',
467 :url => '/tmp/test/bazaar',
457 :log_encoding => 'ISO-8859-1' )
468 :log_encoding => 'ISO-8859-1' )
458 assert r
469 assert r
459 c = Changeset.new(:repository => r,
470 c = Changeset.new(:repository => r,
460 :committed_on => Time.now,
471 :committed_on => Time.now,
461 :revision => '123',
472 :revision => '123',
462 :scmid => '12345',
473 :scmid => '12345',
463 :comments => s1)
474 :comments => s1)
464 assert( c.save )
475 assert( c.save )
465 assert_equal s4, c.comments
476 assert_equal s4, c.comments
466 end
477 end
467
478
468 def test_invalid_utf8_sequences_in_paths_should_be_replaced
479 def test_invalid_utf8_sequences_in_paths_should_be_replaced
469 proj = Project.find(3)
480 proj = Project.find(3)
470 str1 = "Texte encod\xe9 en ISO-8859-1"
481 str1 = "Texte encod\xe9 en ISO-8859-1"
471 str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
482 str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test"
472 str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
483 str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding)
473 str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
484 str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding)
474 r = Repository::Bazaar.create!(
485 r = Repository::Bazaar.create!(
475 :project => proj,
486 :project => proj,
476 :url => '/tmp/test/bazaar',
487 :url => '/tmp/test/bazaar',
477 :log_encoding => 'UTF-8' )
488 :log_encoding => 'UTF-8' )
478 assert r
489 assert r
479 cs = Changeset.new(
490 cs = Changeset.new(
480 :repository => r,
491 :repository => r,
481 :committed_on => Time.now,
492 :committed_on => Time.now,
482 :revision => '123',
493 :revision => '123',
483 :scmid => '12345',
494 :scmid => '12345',
484 :comments => "test")
495 :comments => "test")
485 assert(cs.save)
496 assert(cs.save)
486 ch = Change.new(
497 ch = Change.new(
487 :changeset => cs,
498 :changeset => cs,
488 :action => "A",
499 :action => "A",
489 :path => str1,
500 :path => str1,
490 :from_path => str2,
501 :from_path => str2,
491 :from_revision => "345")
502 :from_revision => "345")
492 assert(ch.save)
503 assert(ch.save)
493 assert_equal "Texte encod? en ISO-8859-1", ch.path
504 assert_equal "Texte encod? en ISO-8859-1", ch.path
494 assert_equal "?a?b?c?d?e test", ch.from_path
505 assert_equal "?a?b?c?d?e test", ch.from_path
495 end
506 end
496
507
497 def test_comments_nil
508 def test_comments_nil
498 proj = Project.find(3)
509 proj = Project.find(3)
499 r = Repository::Bazaar.create!(
510 r = Repository::Bazaar.create!(
500 :project => proj,
511 :project => proj,
501 :url => '/tmp/test/bazaar',
512 :url => '/tmp/test/bazaar',
502 :log_encoding => 'ISO-8859-1' )
513 :log_encoding => 'ISO-8859-1' )
503 assert r
514 assert r
504 c = Changeset.new(:repository => r,
515 c = Changeset.new(:repository => r,
505 :committed_on => Time.now,
516 :committed_on => Time.now,
506 :revision => '123',
517 :revision => '123',
507 :scmid => '12345',
518 :scmid => '12345',
508 :comments => nil,
519 :comments => nil,
509 :committer => nil)
520 :committer => nil)
510 assert( c.save )
521 assert( c.save )
511 assert_equal "", c.comments
522 assert_equal "", c.comments
512 assert_equal nil, c.committer
523 assert_equal nil, c.committer
513 if c.comments.respond_to?(:force_encoding)
524 if c.comments.respond_to?(:force_encoding)
514 assert_equal "UTF-8", c.comments.encoding.to_s
525 assert_equal "UTF-8", c.comments.encoding.to_s
515 end
526 end
516 end
527 end
517
528
518 def test_comments_empty
529 def test_comments_empty
519 proj = Project.find(3)
530 proj = Project.find(3)
520 r = Repository::Bazaar.create!(
531 r = Repository::Bazaar.create!(
521 :project => proj,
532 :project => proj,
522 :url => '/tmp/test/bazaar',
533 :url => '/tmp/test/bazaar',
523 :log_encoding => 'ISO-8859-1' )
534 :log_encoding => 'ISO-8859-1' )
524 assert r
535 assert r
525 c = Changeset.new(:repository => r,
536 c = Changeset.new(:repository => r,
526 :committed_on => Time.now,
537 :committed_on => Time.now,
527 :revision => '123',
538 :revision => '123',
528 :scmid => '12345',
539 :scmid => '12345',
529 :comments => "",
540 :comments => "",
530 :committer => "")
541 :committer => "")
531 assert( c.save )
542 assert( c.save )
532 assert_equal "", c.comments
543 assert_equal "", c.comments
533 assert_equal "", c.committer
544 assert_equal "", c.committer
534 if c.comments.respond_to?(:force_encoding)
545 if c.comments.respond_to?(:force_encoding)
535 assert_equal "UTF-8", c.comments.encoding.to_s
546 assert_equal "UTF-8", c.comments.encoding.to_s
536 assert_equal "UTF-8", c.committer.encoding.to_s
547 assert_equal "UTF-8", c.committer.encoding.to_s
537 end
548 end
538 end
549 end
539
550
540 def test_comments_should_accept_more_than_64k
551 def test_comments_should_accept_more_than_64k
541 c = Changeset.new(:repository => Repository.first,
552 c = Changeset.new(:repository => Repository.first,
542 :committed_on => Time.now,
553 :committed_on => Time.now,
543 :revision => '123',
554 :revision => '123',
544 :scmid => '12345',
555 :scmid => '12345',
545 :comments => "a" * 500.kilobyte)
556 :comments => "a" * 500.kilobyte)
546 assert c.save
557 assert c.save
547 c.reload
558 c.reload
548 assert_equal 500.kilobyte, c.comments.size
559 assert_equal 500.kilobyte, c.comments.size
549 end
560 end
550
561
551 def test_identifier
562 def test_identifier
552 c = Changeset.find_by_revision('1')
563 c = Changeset.find_by_revision('1')
553 assert_equal c.revision, c.identifier
564 assert_equal c.revision, c.identifier
554 end
565 end
555 end
566 end
General Comments 0
You need to be logged in to leave comments. Login now