##// END OF EJS Templates
code format cleanup app/models/changeset.rb...
Toshi MARUYAMA -
r12001:3109ed9320b1
parent child
Show More
@@ -1,278 +1,282
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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
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 tag = if scmid?
157 tag = if scmid?
158 "commit:#{scmid}"
158 "commit:#{scmid}"
159 else
159 else
160 "r#{revision}"
160 "r#{revision}"
161 end
161 end
162 if repository && repository.identifier.present?
162 if repository && repository.identifier.present?
163 tag = "#{repository.identifier}|#{tag}"
163 tag = "#{repository.identifier}|#{tag}"
164 end
164 end
165 if ref_project && project && ref_project != project
165 if ref_project && project && ref_project != project
166 tag = "#{project.identifier}:#{tag}"
166 tag = "#{project.identifier}:#{tag}"
167 end
167 end
168 tag
168 tag
169 end
169 end
170
170
171 # Returns the title used for the changeset in the activity/search results
171 # Returns the title used for the changeset in the activity/search results
172 def title
172 def title
173 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
173 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
174 comm = short_comments.blank? ? '' : (': ' + short_comments)
174 comm = short_comments.blank? ? '' : (': ' + short_comments)
175 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
175 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
176 end
176 end
177
177
178 # Returns the previous changeset
178 # Returns the previous changeset
179 def previous
179 def previous
180 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
180 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
181 end
181 end
182
182
183 # Returns the next changeset
183 # Returns the next changeset
184 def next
184 def next
185 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
185 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
186 end
186 end
187
187
188 # Creates a new Change from it's common parameters
188 # Creates a new Change from it's common parameters
189 def create_change(change)
189 def create_change(change)
190 Change.create(:changeset => self,
190 Change.create(:changeset => self,
191 :action => change[:action],
191 :action => change[:action],
192 :path => change[:path],
192 :path => change[:path],
193 :from_path => change[:from_path],
193 :from_path => change[:from_path],
194 :from_revision => change[:from_revision])
194 :from_revision => change[:from_revision])
195 end
195 end
196
196
197 # Finds an issue that can be referenced by the commit message
197 # Finds an issue that can be referenced by the commit message
198 def find_referenced_issue_by_id(id)
198 def find_referenced_issue_by_id(id)
199 return nil if id.blank?
199 return nil if id.blank?
200 issue = Issue.find_by_id(id.to_i, :include => :project)
200 issue = Issue.find_by_id(id.to_i, :include => :project)
201 if Setting.commit_cross_project_ref?
201 if Setting.commit_cross_project_ref?
202 # all issues can be referenced/fixed
202 # all issues can be referenced/fixed
203 elsif issue
203 elsif issue
204 # issue that belong to the repository project, a subproject or a parent project only
204 # issue that belong to the repository project, a subproject or a parent project only
205 unless issue.project &&
205 unless issue.project &&
206 (project == issue.project || project.is_ancestor_of?(issue.project) ||
206 (project == issue.project || project.is_ancestor_of?(issue.project) ||
207 project.is_descendant_of?(issue.project))
207 project.is_descendant_of?(issue.project))
208 issue = nil
208 issue = nil
209 end
209 end
210 end
210 end
211 issue
211 issue
212 end
212 end
213
213
214 private
214 private
215
215
216 # Updates the +issue+ according to +action+
216 # Updates the +issue+ according to +action+
217 def fix_issue(issue, action)
217 def fix_issue(issue, action)
218 # the issue may have been updated by the closure of another one (eg. duplicate)
218 # the issue may have been updated by the closure of another one (eg. duplicate)
219 issue.reload
219 issue.reload
220 # don't change the status is the issue is closed
220 # don't change the status is the issue is closed
221 return if issue.status && issue.status.is_closed?
221 return if issue.status && issue.status.is_closed?
222
222
223 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
223 journal = issue.init_journal(user || User.anonymous,
224 ll(Setting.default_language,
225 :text_status_changed_by_changeset,
226 text_tag(issue.project)))
224 rule = Setting.commit_update_keywords_array.detect do |rule|
227 rule = Setting.commit_update_keywords_array.detect do |rule|
225 rule['keywords'].include?(action) && (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
228 rule['keywords'].include?(action) &&
229 (rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
226 end
230 end
227 if rule
231 if rule
228 issue.assign_attributes rule.slice(*Issue.attribute_names)
232 issue.assign_attributes rule.slice(*Issue.attribute_names)
229 end
233 end
230 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
234 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
231 { :changeset => self, :issue => issue, :action => action })
235 { :changeset => self, :issue => issue, :action => action })
232 unless issue.save
236 unless issue.save
233 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
237 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
234 end
238 end
235 issue
239 issue
236 end
240 end
237
241
238 def log_time(issue, hours)
242 def log_time(issue, hours)
239 time_entry = TimeEntry.new(
243 time_entry = TimeEntry.new(
240 :user => user,
244 :user => user,
241 :hours => hours,
245 :hours => hours,
242 :issue => issue,
246 :issue => issue,
243 :spent_on => commit_date,
247 :spent_on => commit_date,
244 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
248 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
245 :locale => Setting.default_language)
249 :locale => Setting.default_language)
246 )
250 )
247 time_entry.activity = log_time_activity unless log_time_activity.nil?
251 time_entry.activity = log_time_activity unless log_time_activity.nil?
248
252
249 unless time_entry.save
253 unless time_entry.save
250 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
254 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
251 end
255 end
252 time_entry
256 time_entry
253 end
257 end
254
258
255 def log_time_activity
259 def log_time_activity
256 if Setting.commit_logtime_activity_id.to_i > 0
260 if Setting.commit_logtime_activity_id.to_i > 0
257 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
261 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
258 end
262 end
259 end
263 end
260
264
261 def split_comments
265 def split_comments
262 comments =~ /\A(.+?)\r?\n(.*)$/m
266 comments =~ /\A(.+?)\r?\n(.*)$/m
263 @short_comments = $1 || comments
267 @short_comments = $1 || comments
264 @long_comments = $2.to_s.strip
268 @long_comments = $2.to_s.strip
265 return @short_comments, @long_comments
269 return @short_comments, @long_comments
266 end
270 end
267
271
268 public
272 public
269
273
270 # Strips and reencodes a commit log before insertion into the database
274 # Strips and reencodes a commit log before insertion into the database
271 def self.normalize_comments(str, encoding)
275 def self.normalize_comments(str, encoding)
272 Changeset.to_utf8(str.to_s.strip, encoding)
276 Changeset.to_utf8(str.to_s.strip, encoding)
273 end
277 end
274
278
275 def self.to_utf8(str, encoding)
279 def self.to_utf8(str, encoding)
276 Redmine::CodesetUtil.to_utf8(str, encoding)
280 Redmine::CodesetUtil.to_utf8(str, encoding)
277 end
281 end
278 end
282 end
General Comments 0
You need to be logged in to leave comments. Login now