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