##// END OF EJS Templates
Adds repository_id param for activity and search results (#779)....
Jean-Philippe Lang -
r8531:5f7995f97743
parent child
Show More
@@ -1,269 +1,269
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| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
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, :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
154 def text_tag
155 if scmid?
155 if scmid?
156 "commit:#{scmid}"
156 "commit:#{scmid}"
157 else
157 else
158 "r#{revision}"
158 "r#{revision}"
159 end
159 end
160 end
160 end
161
161
162 # Returns the previous changeset
162 # Returns the previous changeset
163 def previous
163 def previous
164 @previous ||= Changeset.find(:first,
164 @previous ||= Changeset.find(:first,
165 :conditions => ['id < ? AND repository_id = ?',
165 :conditions => ['id < ? AND repository_id = ?',
166 self.id, self.repository_id],
166 self.id, self.repository_id],
167 :order => 'id DESC')
167 :order => 'id DESC')
168 end
168 end
169
169
170 # Returns the next changeset
170 # Returns the next changeset
171 def next
171 def next
172 @next ||= Changeset.find(:first,
172 @next ||= Changeset.find(:first,
173 :conditions => ['id > ? AND repository_id = ?',
173 :conditions => ['id > ? AND repository_id = ?',
174 self.id, self.repository_id],
174 self.id, self.repository_id],
175 :order => 'id ASC')
175 :order => 'id ASC')
176 end
176 end
177
177
178 # Creates a new Change from it's common parameters
178 # Creates a new Change from it's common parameters
179 def create_change(change)
179 def create_change(change)
180 Change.create(:changeset => self,
180 Change.create(:changeset => self,
181 :action => change[:action],
181 :action => change[:action],
182 :path => change[:path],
182 :path => change[:path],
183 :from_path => change[:from_path],
183 :from_path => change[:from_path],
184 :from_revision => change[:from_revision])
184 :from_revision => change[:from_revision])
185 end
185 end
186
186
187 private
187 private
188
188
189 # Finds an issue that can be referenced by the commit message
189 # Finds an issue that can be referenced by the commit message
190 # i.e. an issue that belong to the repository project, a subproject or a parent project
190 # i.e. an issue that belong to the repository project, a subproject or a parent project
191 def find_referenced_issue_by_id(id)
191 def find_referenced_issue_by_id(id)
192 return nil if id.blank?
192 return nil if id.blank?
193 issue = Issue.find_by_id(id.to_i, :include => :project)
193 issue = Issue.find_by_id(id.to_i, :include => :project)
194 if issue
194 if issue
195 unless issue.project &&
195 unless issue.project &&
196 (project == issue.project || project.is_ancestor_of?(issue.project) ||
196 (project == issue.project || project.is_ancestor_of?(issue.project) ||
197 project.is_descendant_of?(issue.project))
197 project.is_descendant_of?(issue.project))
198 issue = nil
198 issue = nil
199 end
199 end
200 end
200 end
201 issue
201 issue
202 end
202 end
203
203
204 def fix_issue(issue)
204 def fix_issue(issue)
205 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
205 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
206 if status.nil?
206 if status.nil?
207 logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
207 logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
208 return issue
208 return issue
209 end
209 end
210
210
211 # the issue may have been updated by the closure of another one (eg. duplicate)
211 # the issue may have been updated by the closure of another one (eg. duplicate)
212 issue.reload
212 issue.reload
213 # don't change the status is the issue is closed
213 # don't change the status is the issue is closed
214 return if issue.status && issue.status.is_closed?
214 return if issue.status && issue.status.is_closed?
215
215
216 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
216 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
217 issue.status = status
217 issue.status = status
218 unless Setting.commit_fix_done_ratio.blank?
218 unless Setting.commit_fix_done_ratio.blank?
219 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
219 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
220 end
220 end
221 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
221 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
222 { :changeset => self, :issue => issue })
222 { :changeset => self, :issue => issue })
223 unless issue.save
223 unless issue.save
224 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
224 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
225 end
225 end
226 issue
226 issue
227 end
227 end
228
228
229 def log_time(issue, hours)
229 def log_time(issue, hours)
230 time_entry = TimeEntry.new(
230 time_entry = TimeEntry.new(
231 :user => user,
231 :user => user,
232 :hours => hours,
232 :hours => hours,
233 :issue => issue,
233 :issue => issue,
234 :spent_on => commit_date,
234 :spent_on => commit_date,
235 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
235 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
236 :locale => Setting.default_language)
236 :locale => Setting.default_language)
237 )
237 )
238 time_entry.activity = log_time_activity unless log_time_activity.nil?
238 time_entry.activity = log_time_activity unless log_time_activity.nil?
239
239
240 unless time_entry.save
240 unless time_entry.save
241 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
241 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
242 end
242 end
243 time_entry
243 time_entry
244 end
244 end
245
245
246 def log_time_activity
246 def log_time_activity
247 if Setting.commit_logtime_activity_id.to_i > 0
247 if Setting.commit_logtime_activity_id.to_i > 0
248 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
248 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
249 end
249 end
250 end
250 end
251
251
252 def split_comments
252 def split_comments
253 comments =~ /\A(.+?)\r?\n(.*)$/m
253 comments =~ /\A(.+?)\r?\n(.*)$/m
254 @short_comments = $1 || comments
254 @short_comments = $1 || comments
255 @long_comments = $2.to_s.strip
255 @long_comments = $2.to_s.strip
256 return @short_comments, @long_comments
256 return @short_comments, @long_comments
257 end
257 end
258
258
259 public
259 public
260
260
261 # Strips and reencodes a commit log before insertion into the database
261 # Strips and reencodes a commit log before insertion into the database
262 def self.normalize_comments(str, encoding)
262 def self.normalize_comments(str, encoding)
263 Changeset.to_utf8(str.to_s.strip, encoding)
263 Changeset.to_utf8(str.to_s.strip, encoding)
264 end
264 end
265
265
266 def self.to_utf8(str, encoding)
266 def self.to_utf8(str, encoding)
267 Redmine::CodesetUtil.to_utf8(str, encoding)
267 Redmine::CodesetUtil.to_utf8(str, encoding)
268 end
268 end
269 end
269 end
General Comments 0
You need to be logged in to leave comments. Login now