##// END OF EJS Templates
Merged r14136 from trunk to 3.0-stable....
Toshi MARUYAMA -
r13763:15067de1afd7
parent child
Show More
@@ -1,299 +1,291
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 :preload => {:repository => :project},
38 :preload => {: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 :scope => preload(:user, {:repository => :project})
44 :scope => preload(: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 attr_protected :id
49 attr_protected :id
50
50
51 scope :visible, lambda {|*args|
51 scope :visible, lambda {|*args|
52 joins(:repository => :project).
52 joins(:repository => :project).
53 where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
53 where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args))
54 }
54 }
55
55
56 after_create :scan_for_issues
56 after_create :scan_for_issues
57 before_create :before_create_cs
57 before_create :before_create_cs
58
58
59 def revision=(r)
59 def revision=(r)
60 write_attribute :revision, (r.nil? ? nil : r.to_s)
60 write_attribute :revision, (r.nil? ? nil : r.to_s)
61 end
61 end
62
62
63 # Returns the identifier of this changeset; depending on repository backends
63 # Returns the identifier of this changeset; depending on repository backends
64 def identifier
64 def identifier
65 if repository.class.respond_to? :changeset_identifier
65 if repository.class.respond_to? :changeset_identifier
66 repository.class.changeset_identifier self
66 repository.class.changeset_identifier self
67 else
67 else
68 revision.to_s
68 revision.to_s
69 end
69 end
70 end
70 end
71
71
72 def committer=(arg)
73 write_attribute :committer,
74 self.class.to_utf8(arg, repository.try(:repo_log_encoding))
75 end
76
77 def comments=(arg)
78 write_attribute :comments,
79 self.class.normalize_comments(arg, repository.try(:repo_log_encoding))
80 end
81
82 def committed_on=(date)
72 def committed_on=(date)
83 self.commit_date = date
73 self.commit_date = date
84 super
74 super
85 end
75 end
86
76
87 # Returns the readable identifier
77 # Returns the readable identifier
88 def format_identifier
78 def format_identifier
89 if repository.class.respond_to? :format_changeset_identifier
79 if repository.class.respond_to? :format_changeset_identifier
90 repository.class.format_changeset_identifier self
80 repository.class.format_changeset_identifier self
91 else
81 else
92 identifier
82 identifier
93 end
83 end
94 end
84 end
95
85
96 def project
86 def project
97 repository.project
87 repository.project
98 end
88 end
99
89
100 def author
90 def author
101 user || committer.to_s.split('<').first
91 user || committer.to_s.split('<').first
102 end
92 end
103
93
104 def before_create_cs
94 def before_create_cs
105 self.comments ||= ''
95 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
96 self.comments = self.class.normalize_comments(
97 self.comments, repository.repo_log_encoding)
106 self.user = repository.find_committer_user(self.committer)
98 self.user = repository.find_committer_user(self.committer)
107 end
99 end
108
100
109 def scan_for_issues
101 def scan_for_issues
110 scan_comment_for_issue_ids
102 scan_comment_for_issue_ids
111 end
103 end
112
104
113 TIMELOG_RE = /
105 TIMELOG_RE = /
114 (
106 (
115 ((\d+)(h|hours?))((\d+)(m|min)?)?
107 ((\d+)(h|hours?))((\d+)(m|min)?)?
116 |
108 |
117 ((\d+)(h|hours?|m|min))
109 ((\d+)(h|hours?|m|min))
118 |
110 |
119 (\d+):(\d+)
111 (\d+):(\d+)
120 |
112 |
121 (\d+([\.,]\d+)?)h?
113 (\d+([\.,]\d+)?)h?
122 )
114 )
123 /x
115 /x
124
116
125 def scan_comment_for_issue_ids
117 def scan_comment_for_issue_ids
126 return if comments.blank?
118 return if comments.blank?
127 # keywords used to reference issues
119 # keywords used to reference issues
128 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
120 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
129 ref_keywords_any = ref_keywords.delete('*')
121 ref_keywords_any = ref_keywords.delete('*')
130 # keywords used to fix issues
122 # keywords used to fix issues
131 fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
123 fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
132
124
133 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
125 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
134
126
135 referenced_issues = []
127 referenced_issues = []
136
128
137 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
129 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
138 action, refs = match[2].to_s.downcase, match[3]
130 action, refs = match[2].to_s.downcase, match[3]
139 next unless action.present? || ref_keywords_any
131 next unless action.present? || ref_keywords_any
140
132
141 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
133 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
142 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
134 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
143 if issue && !issue_linked_to_same_commit?(issue)
135 if issue && !issue_linked_to_same_commit?(issue)
144 referenced_issues << issue
136 referenced_issues << issue
145 # Don't update issues or log time when importing old commits
137 # Don't update issues or log time when importing old commits
146 unless repository.created_on && committed_on && committed_on < repository.created_on
138 unless repository.created_on && committed_on && committed_on < repository.created_on
147 fix_issue(issue, action) if fix_keywords.include?(action)
139 fix_issue(issue, action) if fix_keywords.include?(action)
148 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
140 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
149 end
141 end
150 end
142 end
151 end
143 end
152 end
144 end
153
145
154 referenced_issues.uniq!
146 referenced_issues.uniq!
155 self.issues = referenced_issues unless referenced_issues.empty?
147 self.issues = referenced_issues unless referenced_issues.empty?
156 end
148 end
157
149
158 def short_comments
150 def short_comments
159 @short_comments || split_comments.first
151 @short_comments || split_comments.first
160 end
152 end
161
153
162 def long_comments
154 def long_comments
163 @long_comments || split_comments.last
155 @long_comments || split_comments.last
164 end
156 end
165
157
166 def text_tag(ref_project=nil)
158 def text_tag(ref_project=nil)
167 repo = ""
159 repo = ""
168 if repository && repository.identifier.present?
160 if repository && repository.identifier.present?
169 repo = "#{repository.identifier}|"
161 repo = "#{repository.identifier}|"
170 end
162 end
171 tag = if scmid?
163 tag = if scmid?
172 "commit:#{repo}#{scmid}"
164 "commit:#{repo}#{scmid}"
173 else
165 else
174 "#{repo}r#{revision}"
166 "#{repo}r#{revision}"
175 end
167 end
176 if ref_project && project && ref_project != project
168 if ref_project && project && ref_project != project
177 tag = "#{project.identifier}:#{tag}"
169 tag = "#{project.identifier}:#{tag}"
178 end
170 end
179 tag
171 tag
180 end
172 end
181
173
182 # Returns the title used for the changeset in the activity/search results
174 # Returns the title used for the changeset in the activity/search results
183 def title
175 def title
184 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
176 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
185 comm = short_comments.blank? ? '' : (': ' + short_comments)
177 comm = short_comments.blank? ? '' : (': ' + short_comments)
186 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
178 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
187 end
179 end
188
180
189 # Returns the previous changeset
181 # Returns the previous changeset
190 def previous
182 def previous
191 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
183 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
192 end
184 end
193
185
194 # Returns the next changeset
186 # Returns the next changeset
195 def next
187 def next
196 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
188 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
197 end
189 end
198
190
199 # Creates a new Change from it's common parameters
191 # Creates a new Change from it's common parameters
200 def create_change(change)
192 def create_change(change)
201 Change.create(:changeset => self,
193 Change.create(:changeset => self,
202 :action => change[:action],
194 :action => change[:action],
203 :path => change[:path],
195 :path => change[:path],
204 :from_path => change[:from_path],
196 :from_path => change[:from_path],
205 :from_revision => change[:from_revision])
197 :from_revision => change[:from_revision])
206 end
198 end
207
199
208 # Finds an issue that can be referenced by the commit message
200 # Finds an issue that can be referenced by the commit message
209 def find_referenced_issue_by_id(id)
201 def find_referenced_issue_by_id(id)
210 return nil if id.blank?
202 return nil if id.blank?
211 issue = Issue.includes(:project).where(:id => id.to_i).first
203 issue = Issue.includes(:project).where(:id => id.to_i).first
212 if Setting.commit_cross_project_ref?
204 if Setting.commit_cross_project_ref?
213 # all issues can be referenced/fixed
205 # all issues can be referenced/fixed
214 elsif issue
206 elsif issue
215 # issue that belong to the repository project, a subproject or a parent project only
207 # issue that belong to the repository project, a subproject or a parent project only
216 unless issue.project &&
208 unless issue.project &&
217 (project == issue.project || project.is_ancestor_of?(issue.project) ||
209 (project == issue.project || project.is_ancestor_of?(issue.project) ||
218 project.is_descendant_of?(issue.project))
210 project.is_descendant_of?(issue.project))
219 issue = nil
211 issue = nil
220 end
212 end
221 end
213 end
222 issue
214 issue
223 end
215 end
224
216
225 private
217 private
226
218
227 # Returns true if the issue is already linked to the same commit
219 # Returns true if the issue is already linked to the same commit
228 # from a different repository
220 # from a different repository
229 def issue_linked_to_same_commit?(issue)
221 def issue_linked_to_same_commit?(issue)
230 repository.same_commits_in_scope(issue.changesets, self).any?
222 repository.same_commits_in_scope(issue.changesets, self).any?
231 end
223 end
232
224
233 # Updates the +issue+ according to +action+
225 # Updates the +issue+ according to +action+
234 def fix_issue(issue, action)
226 def fix_issue(issue, action)
235 # the issue may have been updated by the closure of another one (eg. duplicate)
227 # the issue may have been updated by the closure of another one (eg. duplicate)
236 issue.reload
228 issue.reload
237 # don't change the status is the issue is closed
229 # don't change the status is the issue is closed
238 return if issue.closed?
230 return if issue.closed?
239
231
240 journal = issue.init_journal(user || User.anonymous,
232 journal = issue.init_journal(user || User.anonymous,
241 ll(Setting.default_language,
233 ll(Setting.default_language,
242 :text_status_changed_by_changeset,
234 :text_status_changed_by_changeset,
243 text_tag(issue.project)))
235 text_tag(issue.project)))
244 rule = Setting.commit_update_keywords_array.detect do |rule|
236 rule = Setting.commit_update_keywords_array.detect do |rule|
245 rule['keywords'].include?(action) &&
237 rule['keywords'].include?(action) &&
246 (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
238 (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
247 end
239 end
248 if rule
240 if rule
249 issue.assign_attributes rule.slice(*Issue.attribute_names)
241 issue.assign_attributes rule.slice(*Issue.attribute_names)
250 end
242 end
251 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
243 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
252 { :changeset => self, :issue => issue, :action => action })
244 { :changeset => self, :issue => issue, :action => action })
253 unless issue.save
245 unless issue.save
254 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
246 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
255 end
247 end
256 issue
248 issue
257 end
249 end
258
250
259 def log_time(issue, hours)
251 def log_time(issue, hours)
260 time_entry = TimeEntry.new(
252 time_entry = TimeEntry.new(
261 :user => user,
253 :user => user,
262 :hours => hours,
254 :hours => hours,
263 :issue => issue,
255 :issue => issue,
264 :spent_on => commit_date,
256 :spent_on => commit_date,
265 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
257 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
266 :locale => Setting.default_language)
258 :locale => Setting.default_language)
267 )
259 )
268 time_entry.activity = log_time_activity unless log_time_activity.nil?
260 time_entry.activity = log_time_activity unless log_time_activity.nil?
269
261
270 unless time_entry.save
262 unless time_entry.save
271 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
263 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
272 end
264 end
273 time_entry
265 time_entry
274 end
266 end
275
267
276 def log_time_activity
268 def log_time_activity
277 if Setting.commit_logtime_activity_id.to_i > 0
269 if Setting.commit_logtime_activity_id.to_i > 0
278 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
270 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
279 end
271 end
280 end
272 end
281
273
282 def split_comments
274 def split_comments
283 comments =~ /\A(.+?)\r?\n(.*)$/m
275 comments =~ /\A(.+?)\r?\n(.*)$/m
284 @short_comments = $1 || comments
276 @short_comments = $1 || comments
285 @long_comments = $2.to_s.strip
277 @long_comments = $2.to_s.strip
286 return @short_comments, @long_comments
278 return @short_comments, @long_comments
287 end
279 end
288
280
289 public
281 public
290
282
291 # Strips and reencodes a commit log before insertion into the database
283 # Strips and reencodes a commit log before insertion into the database
292 def self.normalize_comments(str, encoding)
284 def self.normalize_comments(str, encoding)
293 Changeset.to_utf8(str.to_s.strip, encoding)
285 Changeset.to_utf8(str.to_s.strip, encoding)
294 end
286 end
295
287
296 def self.to_utf8(str, encoding)
288 def self.to_utf8(str, encoding)
297 Redmine::CodesetUtil.to_utf8(str, encoding)
289 Redmine::CodesetUtil.to_utf8(str, encoding)
298 end
290 end
299 end
291 end
General Comments 0
You need to be logged in to leave comments. Login now