##// END OF EJS Templates
Adds the repository identifier in the activity and search results (#779)....
Jean-Philippe Lang -
r9137:ee8002b0c934
parent child
Show More
@@ -1,278 +1,285
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 :changes, :dependent => :delete_all
23 has_many :changes, :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| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
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 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
52 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
53 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
53 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
54
54
55 after_create :scan_for_issues
55 after_create :scan_for_issues
56 before_create :before_create_cs
56 before_create :before_create_cs
57
57
58 def revision=(r)
58 def revision=(r)
59 write_attribute :revision, (r.nil? ? nil : r.to_s)
59 write_attribute :revision, (r.nil? ? nil : r.to_s)
60 end
60 end
61
61
62 # Returns the identifier of this changeset; depending on repository backends
62 # Returns the identifier of this changeset; depending on repository backends
63 def identifier
63 def identifier
64 if repository.class.respond_to? :changeset_identifier
64 if repository.class.respond_to? :changeset_identifier
65 repository.class.changeset_identifier self
65 repository.class.changeset_identifier self
66 else
66 else
67 revision.to_s
67 revision.to_s
68 end
68 end
69 end
69 end
70
70
71 def committed_on=(date)
71 def committed_on=(date)
72 self.commit_date = date
72 self.commit_date = date
73 super
73 super
74 end
74 end
75
75
76 # Returns the readable identifier
76 # Returns the readable identifier
77 def format_identifier
77 def format_identifier
78 if repository.class.respond_to? :format_changeset_identifier
78 if repository.class.respond_to? :format_changeset_identifier
79 repository.class.format_changeset_identifier self
79 repository.class.format_changeset_identifier self
80 else
80 else
81 identifier
81 identifier
82 end
82 end
83 end
83 end
84
84
85 def project
85 def project
86 repository.project
86 repository.project
87 end
87 end
88
88
89 def author
89 def author
90 user || committer.to_s.split('<').first
90 user || committer.to_s.split('<').first
91 end
91 end
92
92
93 def before_create_cs
93 def before_create_cs
94 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
94 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
95 self.comments = self.class.normalize_comments(
95 self.comments = self.class.normalize_comments(
96 self.comments, repository.repo_log_encoding)
96 self.comments, repository.repo_log_encoding)
97 self.user = repository.find_committer_user(self.committer)
97 self.user = repository.find_committer_user(self.committer)
98 end
98 end
99
99
100 def scan_for_issues
100 def scan_for_issues
101 scan_comment_for_issue_ids
101 scan_comment_for_issue_ids
102 end
102 end
103
103
104 TIMELOG_RE = /
104 TIMELOG_RE = /
105 (
105 (
106 ((\d+)(h|hours?))((\d+)(m|min)?)?
106 ((\d+)(h|hours?))((\d+)(m|min)?)?
107 |
107 |
108 ((\d+)(h|hours?|m|min))
108 ((\d+)(h|hours?|m|min))
109 |
109 |
110 (\d+):(\d+)
110 (\d+):(\d+)
111 |
111 |
112 (\d+([\.,]\d+)?)h?
112 (\d+([\.,]\d+)?)h?
113 )
113 )
114 /x
114 /x
115
115
116 def scan_comment_for_issue_ids
116 def scan_comment_for_issue_ids
117 return if comments.blank?
117 return if comments.blank?
118 # keywords used to reference issues
118 # keywords used to reference issues
119 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
119 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
120 ref_keywords_any = ref_keywords.delete('*')
120 ref_keywords_any = ref_keywords.delete('*')
121 # keywords used to fix issues
121 # keywords used to fix issues
122 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
122 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
123
123
124 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
124 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
125
125
126 referenced_issues = []
126 referenced_issues = []
127
127
128 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
128 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
129 action, refs = match[2], match[3]
129 action, refs = match[2], match[3]
130 next unless action.present? || ref_keywords_any
130 next unless action.present? || ref_keywords_any
131
131
132 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
132 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
133 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
133 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
134 if issue
134 if issue
135 referenced_issues << issue
135 referenced_issues << issue
136 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
136 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
137 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
137 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
138 end
138 end
139 end
139 end
140 end
140 end
141
141
142 referenced_issues.uniq!
142 referenced_issues.uniq!
143 self.issues = referenced_issues unless referenced_issues.empty?
143 self.issues = referenced_issues unless referenced_issues.empty?
144 end
144 end
145
145
146 def short_comments
146 def short_comments
147 @short_comments || split_comments.first
147 @short_comments || split_comments.first
148 end
148 end
149
149
150 def long_comments
150 def long_comments
151 @long_comments || split_comments.last
151 @long_comments || split_comments.last
152 end
152 end
153
153
154 def text_tag(ref_project=nil)
154 def text_tag(ref_project=nil)
155 tag = if scmid?
155 tag = if scmid?
156 "commit:#{scmid}"
156 "commit:#{scmid}"
157 else
157 else
158 "r#{revision}"
158 "r#{revision}"
159 end
159 end
160 if repository && repository.identifier.present?
160 if repository && repository.identifier.present?
161 tag = "#{repository.identifier}|#{tag}"
161 tag = "#{repository.identifier}|#{tag}"
162 end
162 end
163 if ref_project && project && ref_project != project
163 if ref_project && project && ref_project != project
164 tag = "#{project.identifier}:#{tag}"
164 tag = "#{project.identifier}:#{tag}"
165 end
165 end
166 tag
166 tag
167 end
167 end
168
168
169 # Returns the title used for the changeset in the activity/search results
170 def title
171 repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
172 comm = short_comments.blank? ? '' : (': ' + short_comments)
173 "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
174 end
175
169 # Returns the previous changeset
176 # Returns the previous changeset
170 def previous
177 def previous
171 @previous ||= Changeset.find(:first,
178 @previous ||= Changeset.find(:first,
172 :conditions => ['id < ? AND repository_id = ?',
179 :conditions => ['id < ? AND repository_id = ?',
173 self.id, self.repository_id],
180 self.id, self.repository_id],
174 :order => 'id DESC')
181 :order => 'id DESC')
175 end
182 end
176
183
177 # Returns the next changeset
184 # Returns the next changeset
178 def next
185 def next
179 @next ||= Changeset.find(:first,
186 @next ||= Changeset.find(:first,
180 :conditions => ['id > ? AND repository_id = ?',
187 :conditions => ['id > ? AND repository_id = ?',
181 self.id, self.repository_id],
188 self.id, self.repository_id],
182 :order => 'id ASC')
189 :order => 'id ASC')
183 end
190 end
184
191
185 # Creates a new Change from it's common parameters
192 # Creates a new Change from it's common parameters
186 def create_change(change)
193 def create_change(change)
187 Change.create(:changeset => self,
194 Change.create(:changeset => self,
188 :action => change[:action],
195 :action => change[:action],
189 :path => change[:path],
196 :path => change[:path],
190 :from_path => change[:from_path],
197 :from_path => change[:from_path],
191 :from_revision => change[:from_revision])
198 :from_revision => change[:from_revision])
192 end
199 end
193
200
194 # Finds an issue that can be referenced by the commit message
201 # Finds an issue that can be referenced by the commit message
195 def find_referenced_issue_by_id(id)
202 def find_referenced_issue_by_id(id)
196 return nil if id.blank?
203 return nil if id.blank?
197 issue = Issue.find_by_id(id.to_i, :include => :project)
204 issue = Issue.find_by_id(id.to_i, :include => :project)
198 if Setting.commit_cross_project_ref?
205 if Setting.commit_cross_project_ref?
199 # all issues can be referenced/fixed
206 # all issues can be referenced/fixed
200 elsif issue
207 elsif issue
201 # issue that belong to the repository project, a subproject or a parent project only
208 # issue that belong to the repository project, a subproject or a parent project only
202 unless issue.project &&
209 unless issue.project &&
203 (project == issue.project || project.is_ancestor_of?(issue.project) ||
210 (project == issue.project || project.is_ancestor_of?(issue.project) ||
204 project.is_descendant_of?(issue.project))
211 project.is_descendant_of?(issue.project))
205 issue = nil
212 issue = nil
206 end
213 end
207 end
214 end
208 issue
215 issue
209 end
216 end
210
217
211 private
218 private
212
219
213 def fix_issue(issue)
220 def fix_issue(issue)
214 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
221 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
215 if status.nil?
222 if status.nil?
216 logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
223 logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
217 return issue
224 return issue
218 end
225 end
219
226
220 # the issue may have been updated by the closure of another one (eg. duplicate)
227 # the issue may have been updated by the closure of another one (eg. duplicate)
221 issue.reload
228 issue.reload
222 # don't change the status is the issue is closed
229 # don't change the status is the issue is closed
223 return if issue.status && issue.status.is_closed?
230 return if issue.status && issue.status.is_closed?
224
231
225 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
232 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
226 issue.status = status
233 issue.status = status
227 unless Setting.commit_fix_done_ratio.blank?
234 unless Setting.commit_fix_done_ratio.blank?
228 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
235 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
229 end
236 end
230 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
237 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
231 { :changeset => self, :issue => issue })
238 { :changeset => self, :issue => issue })
232 unless issue.save
239 unless issue.save
233 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
240 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
234 end
241 end
235 issue
242 issue
236 end
243 end
237
244
238 def log_time(issue, hours)
245 def log_time(issue, hours)
239 time_entry = TimeEntry.new(
246 time_entry = TimeEntry.new(
240 :user => user,
247 :user => user,
241 :hours => hours,
248 :hours => hours,
242 :issue => issue,
249 :issue => issue,
243 :spent_on => commit_date,
250 :spent_on => commit_date,
244 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
251 :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
245 :locale => Setting.default_language)
252 :locale => Setting.default_language)
246 )
253 )
247 time_entry.activity = log_time_activity unless log_time_activity.nil?
254 time_entry.activity = log_time_activity unless log_time_activity.nil?
248
255
249 unless time_entry.save
256 unless time_entry.save
250 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
257 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
251 end
258 end
252 time_entry
259 time_entry
253 end
260 end
254
261
255 def log_time_activity
262 def log_time_activity
256 if Setting.commit_logtime_activity_id.to_i > 0
263 if Setting.commit_logtime_activity_id.to_i > 0
257 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
264 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
258 end
265 end
259 end
266 end
260
267
261 def split_comments
268 def split_comments
262 comments =~ /\A(.+?)\r?\n(.*)$/m
269 comments =~ /\A(.+?)\r?\n(.*)$/m
263 @short_comments = $1 || comments
270 @short_comments = $1 || comments
264 @long_comments = $2.to_s.strip
271 @long_comments = $2.to_s.strip
265 return @short_comments, @long_comments
272 return @short_comments, @long_comments
266 end
273 end
267
274
268 public
275 public
269
276
270 # Strips and reencodes a commit log before insertion into the database
277 # Strips and reencodes a commit log before insertion into the database
271 def self.normalize_comments(str, encoding)
278 def self.normalize_comments(str, encoding)
272 Changeset.to_utf8(str.to_s.strip, encoding)
279 Changeset.to_utf8(str.to_s.strip, encoding)
273 end
280 end
274
281
275 def self.to_utf8(str, encoding)
282 def self.to_utf8(str, encoding)
276 Redmine::CodesetUtil.to_utf8(str, encoding)
283 Redmine::CodesetUtil.to_utf8(str, encoding)
277 end
284 end
278 end
285 end
General Comments 0
You need to be logged in to leave comments. Login now