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