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