##// END OF EJS Templates
remove trailing white-space from app/models/changeset.rb...
Toshi MARUYAMA -
r9839:07546b494306
parent child
Show More
@@ -1,280 +1,280
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'iconv'
18 require 'iconv'
19
19
20 class Changeset < ActiveRecord::Base
20 class Changeset < ActiveRecord::Base
21 belongs_to :repository
21 belongs_to :repository
22 belongs_to :user
22 belongs_to :user
23 has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
23 has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
24 has_and_belongs_to_many :issues
24 has_and_belongs_to_many :issues
25 has_and_belongs_to_many :parents,
25 has_and_belongs_to_many :parents,
26 :class_name => "Changeset",
26 :class_name => "Changeset",
27 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
27 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
28 :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
28 :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
29 has_and_belongs_to_many :children,
29 has_and_belongs_to_many :children,
30 :class_name => "Changeset",
30 :class_name => "Changeset",
31 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
31 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
32 :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
32 :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
33
33
34 acts_as_event :title => Proc.new {|o| o.title},
34 acts_as_event :title => Proc.new {|o| o.title},
35 :description => :long_comments,
35 :description => :long_comments,
36 :datetime => :committed_on,
36 :datetime => :committed_on,
37 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
37 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
38
38
39 acts_as_searchable :columns => 'comments',
39 acts_as_searchable :columns => 'comments',
40 :include => {:repository => :project},
40 :include => {:repository => :project},
41 :project_key => "#{Repository.table_name}.project_id",
41 :project_key => "#{Repository.table_name}.project_id",
42 :date_column => 'committed_on'
42 :date_column => 'committed_on'
43
43
44 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
44 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
45 :author_key => :user_id,
45 :author_key => :user_id,
46 :find_options => {:include => [:user, {:repository => :project}]}
46 :find_options => {:include => [:user, {:repository => :project}]}
47
47
48 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
48 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
49 validates_uniqueness_of :revision, :scope => :repository_id
49 validates_uniqueness_of :revision, :scope => :repository_id
50 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
50 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
51
51
52 scope :visible,
52 scope :visible,
53 lambda {|*args| { :include => {:repository => :project},
53 lambda {|*args| { :include => {:repository => :project},
54 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
54 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
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 committed_on=(date)
72 def committed_on=(date)
73 self.commit_date = date
73 self.commit_date = date
74 super
74 super
75 end
75 end
76
76
77 # Returns the readable identifier
77 # Returns the readable identifier
78 def format_identifier
78 def format_identifier
79 if repository.class.respond_to? :format_changeset_identifier
79 if repository.class.respond_to? :format_changeset_identifier
80 repository.class.format_changeset_identifier self
80 repository.class.format_changeset_identifier self
81 else
81 else
82 identifier
82 identifier
83 end
83 end
84 end
84 end
85
85
86 def project
86 def project
87 repository.project
87 repository.project
88 end
88 end
89
89
90 def author
90 def author
91 user || committer.to_s.split('<').first
91 user || committer.to_s.split('<').first
92 end
92 end
93
93
94 def before_create_cs
94 def before_create_cs
95 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
95 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
96 self.comments = self.class.normalize_comments(
96 self.comments = self.class.normalize_comments(
97 self.comments, repository.repo_log_encoding)
97 self.comments, repository.repo_log_encoding)
98 self.user = repository.find_committer_user(self.committer)
98 self.user = repository.find_committer_user(self.committer)
99 end
99 end
100
100
101 def scan_for_issues
101 def scan_for_issues
102 scan_comment_for_issue_ids
102 scan_comment_for_issue_ids
103 end
103 end
104
104
105 TIMELOG_RE = /
105 TIMELOG_RE = /
106 (
106 (
107 ((\d+)(h|hours?))((\d+)(m|min)?)?
107 ((\d+)(h|hours?))((\d+)(m|min)?)?
108 |
108 |
109 ((\d+)(h|hours?|m|min))
109 ((\d+)(h|hours?|m|min))
110 |
110 |
111 (\d+):(\d+)
111 (\d+):(\d+)
112 |
112 |
113 (\d+([\.,]\d+)?)h?
113 (\d+([\.,]\d+)?)h?
114 )
114 )
115 /x
115 /x
116
116
117 def scan_comment_for_issue_ids
117 def scan_comment_for_issue_ids
118 return if comments.blank?
118 return if comments.blank?
119 # keywords used to reference issues
119 # keywords used to reference issues
120 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
120 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
121 ref_keywords_any = ref_keywords.delete('*')
121 ref_keywords_any = ref_keywords.delete('*')
122 # keywords used to fix issues
122 # keywords used to fix issues
123 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
123 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
124
124
125 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("|")
126
126
127 referenced_issues = []
127 referenced_issues = []
128
128
129 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|
130 action, refs = match[2], match[3]
130 action, refs = match[2], match[3]
131 next unless action.present? || ref_keywords_any
131 next unless action.present? || ref_keywords_any
132
132
133 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
133 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
134 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]
135 if issue
135 if issue
136 referenced_issues << issue
136 referenced_issues << issue
137 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
137 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
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
142
143 referenced_issues.uniq!
143 referenced_issues.uniq!
144 self.issues = referenced_issues unless referenced_issues.empty?
144 self.issues = referenced_issues unless referenced_issues.empty?
145 end
145 end
146
146
147 def short_comments
147 def short_comments
148 @short_comments || split_comments.first
148 @short_comments || split_comments.first
149 end
149 end
150
150
151 def long_comments
151 def long_comments
152 @long_comments || split_comments.last
152 @long_comments || split_comments.last
153 end
153 end
154
154
155 def text_tag(ref_project=nil)
155 def text_tag(ref_project=nil)
156 tag = if scmid?
156 tag = if scmid?
157 "commit:#{scmid}"
157 "commit:#{scmid}"
158 else
158 else
159 "r#{revision}"
159 "r#{revision}"
160 end
160 end
161 if repository && repository.identifier.present?
161 if repository && repository.identifier.present?
162 tag = "#{repository.identifier}|#{tag}"
162 tag = "#{repository.identifier}|#{tag}"
163 end
163 end
164 if ref_project && project && ref_project != project
164 if ref_project && project && ref_project != project
165 tag = "#{project.identifier}:#{tag}"
165 tag = "#{project.identifier}:#{tag}"
166 end
166 end
167 tag
167 tag
168 end
168 end
169
169
170 # Returns the title used for the changeset in the activity/search results
170 # Returns the title used for the changeset in the activity/search results
171 def title
171 def title
172 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
172 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
173 comm = short_comments.blank? ? '' : (': ' + short_comments)
173 comm = short_comments.blank? ? '' : (': ' + short_comments)
174 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
174 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
175 end
175 end
176
176
177 # Returns the previous changeset
177 # Returns the previous changeset
178 def previous
178 def previous
179 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
179 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
180 end
180 end
181
181
182 # Returns the next changeset
182 # Returns the next changeset
183 def next
183 def next
184 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
184 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
185 end
185 end
186
186
187 # Creates a new Change from it's common parameters
187 # Creates a new Change from it's common parameters
188 def create_change(change)
188 def create_change(change)
189 Change.create(:changeset => self,
189 Change.create(:changeset => self,
190 :action => change[:action],
190 :action => change[:action],
191 :path => change[:path],
191 :path => change[:path],
192 :from_path => change[:from_path],
192 :from_path => change[:from_path],
193 :from_revision => change[:from_revision])
193 :from_revision => change[:from_revision])
194 end
194 end
195
195
196 # Finds an issue that can be referenced by the commit message
196 # Finds an issue that can be referenced by the commit message
197 def find_referenced_issue_by_id(id)
197 def find_referenced_issue_by_id(id)
198 return nil if id.blank?
198 return nil if id.blank?
199 issue = Issue.find_by_id(id.to_i, :include => :project)
199 issue = Issue.find_by_id(id.to_i, :include => :project)
200 if Setting.commit_cross_project_ref?
200 if Setting.commit_cross_project_ref?
201 # all issues can be referenced/fixed
201 # all issues can be referenced/fixed
202 elsif issue
202 elsif issue
203 # issue that belong to the repository project, a subproject or a parent project only
203 # issue that belong to the repository project, a subproject or a parent project only
204 unless issue.project &&
204 unless issue.project &&
205 (project == issue.project || project.is_ancestor_of?(issue.project) ||
205 (project == issue.project || project.is_ancestor_of?(issue.project) ||
206 project.is_descendant_of?(issue.project))
206 project.is_descendant_of?(issue.project))
207 issue = nil
207 issue = nil
208 end
208 end
209 end
209 end
210 issue
210 issue
211 end
211 end
212
212
213 private
213 private
214
214
215 def fix_issue(issue)
215 def fix_issue(issue)
216 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
216 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
217 if status.nil?
217 if status.nil?
218 logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
218 logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
219 return issue
219 return issue
220 end
220 end
221
221
222 # the issue may have been updated by the closure of another one (eg. duplicate)
222 # the issue may have been updated by the closure of another one (eg. duplicate)
223 issue.reload
223 issue.reload
224 # don't change the status is the issue is closed
224 # don't change the status is the issue is closed
225 return if issue.status && issue.status.is_closed?
225 return if issue.status && issue.status.is_closed?
226
226
227 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
227 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
228 issue.status = status
228 issue.status = status
229 unless Setting.commit_fix_done_ratio.blank?
229 unless Setting.commit_fix_done_ratio.blank?
230 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
230 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
231 end
231 end
232 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
232 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
233 { :changeset => self, :issue => issue })
233 { :changeset => self, :issue => issue })
234 unless issue.save
234 unless issue.save
235 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
235 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
236 end
236 end
237 issue
237 issue
238 end
238 end
239
239
240 def log_time(issue, hours)
240 def log_time(issue, hours)
241 time_entry = TimeEntry.new(
241 time_entry = TimeEntry.new(
242 :user => user,
242 :user => user,
243 :hours => hours,
243 :hours => hours,
244 :issue => issue,
244 :issue => issue,
245 :spent_on => commit_date,
245 :spent_on => commit_date,
246 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
246 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
247 :locale => Setting.default_language)
247 :locale => Setting.default_language)
248 )
248 )
249 time_entry.activity = log_time_activity unless log_time_activity.nil?
249 time_entry.activity = log_time_activity unless log_time_activity.nil?
250
250
251 unless time_entry.save
251 unless time_entry.save
252 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
252 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
253 end
253 end
254 time_entry
254 time_entry
255 end
255 end
256
256
257 def log_time_activity
257 def log_time_activity
258 if Setting.commit_logtime_activity_id.to_i > 0
258 if Setting.commit_logtime_activity_id.to_i > 0
259 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
259 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
260 end
260 end
261 end
261 end
262
262
263 def split_comments
263 def split_comments
264 comments =~ /\A(.+?)\r?\n(.*)$/m
264 comments =~ /\A(.+?)\r?\n(.*)$/m
265 @short_comments = $1 || comments
265 @short_comments = $1 || comments
266 @long_comments = $2.to_s.strip
266 @long_comments = $2.to_s.strip
267 return @short_comments, @long_comments
267 return @short_comments, @long_comments
268 end
268 end
269
269
270 public
270 public
271
271
272 # Strips and reencodes a commit log before insertion into the database
272 # Strips and reencodes a commit log before insertion into the database
273 def self.normalize_comments(str, encoding)
273 def self.normalize_comments(str, encoding)
274 Changeset.to_utf8(str.to_s.strip, encoding)
274 Changeset.to_utf8(str.to_s.strip, encoding)
275 end
275 end
276
276
277 def self.to_utf8(str, encoding)
277 def self.to_utf8(str, encoding)
278 Redmine::CodesetUtil.to_utf8(str, encoding)
278 Redmine::CodesetUtil.to_utf8(str, encoding)
279 end
279 end
280 end
280 end
General Comments 0
You need to be logged in to leave comments. Login now