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