##// END OF EJS Templates
Pass the commit keyword used to update the issue to the plugin hook....
Jean-Philippe Lang -
r11968:0444ecca3c87
parent child
Show More
@@ -1,273 +1,273
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_by_keyword.keys
121 fix_keywords = Setting.commit_update_by_keyword.keys
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 fix_issue(issue, action) if fix_keywords.include?(action)
135 fix_issue(issue, action) if fix_keywords.include?(action)
136 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
136 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
137 end
137 end
138 end
138 end
139 end
139 end
140
140
141 referenced_issues.uniq!
141 referenced_issues.uniq!
142 self.issues = referenced_issues unless referenced_issues.empty?
142 self.issues = referenced_issues unless referenced_issues.empty?
143 end
143 end
144
144
145 def short_comments
145 def short_comments
146 @short_comments || split_comments.first
146 @short_comments || split_comments.first
147 end
147 end
148
148
149 def long_comments
149 def long_comments
150 @long_comments || split_comments.last
150 @long_comments || split_comments.last
151 end
151 end
152
152
153 def text_tag(ref_project=nil)
153 def text_tag(ref_project=nil)
154 tag = if scmid?
154 tag = if scmid?
155 "commit:#{scmid}"
155 "commit:#{scmid}"
156 else
156 else
157 "r#{revision}"
157 "r#{revision}"
158 end
158 end
159 if repository && repository.identifier.present?
159 if repository && repository.identifier.present?
160 tag = "#{repository.identifier}|#{tag}"
160 tag = "#{repository.identifier}|#{tag}"
161 end
161 end
162 if ref_project && project && ref_project != project
162 if ref_project && project && ref_project != project
163 tag = "#{project.identifier}:#{tag}"
163 tag = "#{project.identifier}:#{tag}"
164 end
164 end
165 tag
165 tag
166 end
166 end
167
167
168 # Returns the title used for the changeset in the activity/search results
168 # Returns the title used for the changeset in the activity/search results
169 def title
169 def title
170 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
170 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
171 comm = short_comments.blank? ? '' : (': ' + short_comments)
171 comm = short_comments.blank? ? '' : (': ' + short_comments)
172 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
172 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
173 end
173 end
174
174
175 # Returns the previous changeset
175 # Returns the previous changeset
176 def previous
176 def previous
177 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
177 @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
178 end
178 end
179
179
180 # Returns the next changeset
180 # Returns the next changeset
181 def next
181 def next
182 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
182 @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
183 end
183 end
184
184
185 # Creates a new Change from it's common parameters
185 # Creates a new Change from it's common parameters
186 def create_change(change)
186 def create_change(change)
187 Change.create(:changeset => self,
187 Change.create(:changeset => self,
188 :action => change[:action],
188 :action => change[:action],
189 :path => change[:path],
189 :path => change[:path],
190 :from_path => change[:from_path],
190 :from_path => change[:from_path],
191 :from_revision => change[:from_revision])
191 :from_revision => change[:from_revision])
192 end
192 end
193
193
194 # Finds an issue that can be referenced by the commit message
194 # Finds an issue that can be referenced by the commit message
195 def find_referenced_issue_by_id(id)
195 def find_referenced_issue_by_id(id)
196 return nil if id.blank?
196 return nil if id.blank?
197 issue = Issue.find_by_id(id.to_i, :include => :project)
197 issue = Issue.find_by_id(id.to_i, :include => :project)
198 if Setting.commit_cross_project_ref?
198 if Setting.commit_cross_project_ref?
199 # all issues can be referenced/fixed
199 # all issues can be referenced/fixed
200 elsif issue
200 elsif issue
201 # issue that belong to the repository project, a subproject or a parent project only
201 # issue that belong to the repository project, a subproject or a parent project only
202 unless issue.project &&
202 unless issue.project &&
203 (project == issue.project || project.is_ancestor_of?(issue.project) ||
203 (project == issue.project || project.is_ancestor_of?(issue.project) ||
204 project.is_descendant_of?(issue.project))
204 project.is_descendant_of?(issue.project))
205 issue = nil
205 issue = nil
206 end
206 end
207 end
207 end
208 issue
208 issue
209 end
209 end
210
210
211 private
211 private
212
212
213 # Updates the +issue+ according to +action+
213 # Updates the +issue+ according to +action+
214 def fix_issue(issue, action)
214 def fix_issue(issue, action)
215 updates = Setting.commit_update_by_keyword[action]
215 updates = Setting.commit_update_by_keyword[action]
216 return unless updates.is_a?(Hash)
216 return unless updates.is_a?(Hash)
217
217
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, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
224 issue.assign_attributes updates.slice(*Issue.attribute_names)
224 issue.assign_attributes updates.slice(*Issue.attribute_names)
225 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
225 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
226 { :changeset => self, :issue => issue })
226 { :changeset => self, :issue => issue, :action => action })
227 unless issue.save
227 unless issue.save
228 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
228 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
229 end
229 end
230 issue
230 issue
231 end
231 end
232
232
233 def log_time(issue, hours)
233 def log_time(issue, hours)
234 time_entry = TimeEntry.new(
234 time_entry = TimeEntry.new(
235 :user => user,
235 :user => user,
236 :hours => hours,
236 :hours => hours,
237 :issue => issue,
237 :issue => issue,
238 :spent_on => commit_date,
238 :spent_on => commit_date,
239 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
239 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
240 :locale => Setting.default_language)
240 :locale => Setting.default_language)
241 )
241 )
242 time_entry.activity = log_time_activity unless log_time_activity.nil?
242 time_entry.activity = log_time_activity unless log_time_activity.nil?
243
243
244 unless time_entry.save
244 unless time_entry.save
245 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
245 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
246 end
246 end
247 time_entry
247 time_entry
248 end
248 end
249
249
250 def log_time_activity
250 def log_time_activity
251 if Setting.commit_logtime_activity_id.to_i > 0
251 if Setting.commit_logtime_activity_id.to_i > 0
252 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
252 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
253 end
253 end
254 end
254 end
255
255
256 def split_comments
256 def split_comments
257 comments =~ /\A(.+?)\r?\n(.*)$/m
257 comments =~ /\A(.+?)\r?\n(.*)$/m
258 @short_comments = $1 || comments
258 @short_comments = $1 || comments
259 @long_comments = $2.to_s.strip
259 @long_comments = $2.to_s.strip
260 return @short_comments, @long_comments
260 return @short_comments, @long_comments
261 end
261 end
262
262
263 public
263 public
264
264
265 # Strips and reencodes a commit log before insertion into the database
265 # Strips and reencodes a commit log before insertion into the database
266 def self.normalize_comments(str, encoding)
266 def self.normalize_comments(str, encoding)
267 Changeset.to_utf8(str.to_s.strip, encoding)
267 Changeset.to_utf8(str.to_s.strip, encoding)
268 end
268 end
269
269
270 def self.to_utf8(str, encoding)
270 def self.to_utf8(str, encoding)
271 Redmine::CodesetUtil.to_utf8(str, encoding)
271 Redmine::CodesetUtil.to_utf8(str, encoding)
272 end
272 end
273 end
273 end
General Comments 0
You need to be logged in to leave comments. Login now