##// END OF EJS Templates
scm: db: model: add parent/child relation for changesets (#5501)...
Toshi MARUYAMA -
r7590:ef1223dd3a1c
parent child
Show More
@@ -0,0 +1,14
1 class CreateChangesetParents < ActiveRecord::Migration
2 def self.up
3 create_table :changeset_parents, :id => false do |t|
4 t.column :changeset_id, :integer, :null => false
5 t.column :parent_id, :integer, :null => false
6 end
7 add_index :changeset_parents, [:changeset_id], :unique => false, :name => :changeset_parents_changeset_ids
8 add_index :changeset_parents, [:parent_id], :unique => false, :name => :changeset_parents_parent_ids
9 end
10
11 def self.down
12 drop_table :changeset_parents
13 end
14 end
@@ -1,301 +1,309
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 has_and_belongs_to_many :parents,
26 :class_name => "Changeset",
27 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
28 :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
29 has_and_belongs_to_many :children,
30 :class_name => "Changeset",
31 :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
32 :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
25 33
26 34 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 35 :description => :long_comments,
28 36 :datetime => :committed_on,
29 37 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
30 38
31 39 acts_as_searchable :columns => 'comments',
32 40 :include => {:repository => :project},
33 41 :project_key => "#{Repository.table_name}.project_id",
34 42 :date_column => 'committed_on'
35 43
36 44 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 45 :author_key => :user_id,
38 46 :find_options => {:include => [:user, {:repository => :project}]}
39 47
40 48 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 49 validates_uniqueness_of :revision, :scope => :repository_id
42 50 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43 51
44 52 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
45 53 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
46 54
47 55 after_create :scan_for_issues
48 56 before_create :before_create_cs
49 57
50 58 def revision=(r)
51 59 write_attribute :revision, (r.nil? ? nil : r.to_s)
52 60 end
53 61
54 62 # Returns the identifier of this changeset; depending on repository backends
55 63 def identifier
56 64 if repository.class.respond_to? :changeset_identifier
57 65 repository.class.changeset_identifier self
58 66 else
59 67 revision.to_s
60 68 end
61 69 end
62 70
63 71 def committed_on=(date)
64 72 self.commit_date = date
65 73 super
66 74 end
67 75
68 76 # Returns the readable identifier
69 77 def format_identifier
70 78 if repository.class.respond_to? :format_changeset_identifier
71 79 repository.class.format_changeset_identifier self
72 80 else
73 81 identifier
74 82 end
75 83 end
76 84
77 85 def project
78 86 repository.project
79 87 end
80 88
81 89 def author
82 90 user || committer.to_s.split('<').first
83 91 end
84 92
85 93 def before_create_cs
86 94 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
87 95 self.comments = self.class.normalize_comments(
88 96 self.comments, repository.repo_log_encoding)
89 97 self.user = repository.find_committer_user(self.committer)
90 98 end
91 99
92 100 def scan_for_issues
93 101 scan_comment_for_issue_ids
94 102 end
95 103
96 104 TIMELOG_RE = /
97 105 (
98 106 ((\d+)(h|hours?))((\d+)(m|min)?)?
99 107 |
100 108 ((\d+)(h|hours?|m|min))
101 109 |
102 110 (\d+):(\d+)
103 111 |
104 112 (\d+([\.,]\d+)?)h?
105 113 )
106 114 /x
107 115
108 116 def scan_comment_for_issue_ids
109 117 return if comments.blank?
110 118 # keywords used to reference issues
111 119 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
112 120 ref_keywords_any = ref_keywords.delete('*')
113 121 # keywords used to fix issues
114 122 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
115 123
116 124 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
117 125
118 126 referenced_issues = []
119 127
120 128 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
121 129 action, refs = match[2], match[3]
122 130 next unless action.present? || ref_keywords_any
123 131
124 132 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
125 133 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
126 134 if issue
127 135 referenced_issues << issue
128 136 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
129 137 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
130 138 end
131 139 end
132 140 end
133 141
134 142 referenced_issues.uniq!
135 143 self.issues = referenced_issues unless referenced_issues.empty?
136 144 end
137 145
138 146 def short_comments
139 147 @short_comments || split_comments.first
140 148 end
141 149
142 150 def long_comments
143 151 @long_comments || split_comments.last
144 152 end
145 153
146 154 def text_tag
147 155 if scmid?
148 156 "commit:#{scmid}"
149 157 else
150 158 "r#{revision}"
151 159 end
152 160 end
153 161
154 162 # Returns the previous changeset
155 163 def previous
156 164 @previous ||= Changeset.find(:first,
157 165 :conditions => ['id < ? AND repository_id = ?',
158 166 self.id, self.repository_id],
159 167 :order => 'id DESC')
160 168 end
161 169
162 170 # Returns the next changeset
163 171 def next
164 172 @next ||= Changeset.find(:first,
165 173 :conditions => ['id > ? AND repository_id = ?',
166 174 self.id, self.repository_id],
167 175 :order => 'id ASC')
168 176 end
169 177
170 178 # Creates a new Change from it's common parameters
171 179 def create_change(change)
172 180 Change.create(:changeset => self,
173 181 :action => change[:action],
174 182 :path => change[:path],
175 183 :from_path => change[:from_path],
176 184 :from_revision => change[:from_revision])
177 185 end
178 186
179 187 private
180 188
181 189 # Finds an issue that can be referenced by the commit message
182 190 # i.e. an issue that belong to the repository project, a subproject or a parent project
183 191 def find_referenced_issue_by_id(id)
184 192 return nil if id.blank?
185 193 issue = Issue.find_by_id(id.to_i, :include => :project)
186 194 if issue
187 195 unless issue.project &&
188 196 (project == issue.project || project.is_ancestor_of?(issue.project) ||
189 197 project.is_descendant_of?(issue.project))
190 198 issue = nil
191 199 end
192 200 end
193 201 issue
194 202 end
195 203
196 204 def fix_issue(issue)
197 205 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
198 206 if status.nil?
199 207 logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
200 208 return issue
201 209 end
202 210
203 211 # the issue may have been updated by the closure of another one (eg. duplicate)
204 212 issue.reload
205 213 # don't change the status is the issue is closed
206 214 return if issue.status && issue.status.is_closed?
207 215
208 216 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
209 217 issue.status = status
210 218 unless Setting.commit_fix_done_ratio.blank?
211 219 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
212 220 end
213 221 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
214 222 { :changeset => self, :issue => issue })
215 223 unless issue.save
216 224 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
217 225 end
218 226 issue
219 227 end
220 228
221 229 def log_time(issue, hours)
222 230 time_entry = TimeEntry.new(
223 231 :user => user,
224 232 :hours => hours,
225 233 :issue => issue,
226 234 :spent_on => commit_date,
227 235 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
228 236 :locale => Setting.default_language)
229 237 )
230 238 time_entry.activity = log_time_activity unless log_time_activity.nil?
231 239
232 240 unless time_entry.save
233 241 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
234 242 end
235 243 time_entry
236 244 end
237 245
238 246 def log_time_activity
239 247 if Setting.commit_logtime_activity_id.to_i > 0
240 248 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
241 249 end
242 250 end
243 251
244 252 def split_comments
245 253 comments =~ /\A(.+?)\r?\n(.*)$/m
246 254 @short_comments = $1 || comments
247 255 @long_comments = $2.to_s.strip
248 256 return @short_comments, @long_comments
249 257 end
250 258
251 259 public
252 260
253 261 # Strips and reencodes a commit log before insertion into the database
254 262 def self.normalize_comments(str, encoding)
255 263 Changeset.to_utf8(str.to_s.strip, encoding)
256 264 end
257 265
258 266 def self.to_utf8(str, encoding)
259 267 return str if str.nil?
260 268 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
261 269 if str.empty?
262 270 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
263 271 return str
264 272 end
265 273 enc = encoding.blank? ? "UTF-8" : encoding
266 274 if str.respond_to?(:force_encoding)
267 275 if enc.upcase != "UTF-8"
268 276 str.force_encoding(enc)
269 277 str = str.encode("UTF-8", :invalid => :replace,
270 278 :undef => :replace, :replace => '?')
271 279 else
272 280 str.force_encoding("UTF-8")
273 281 if ! str.valid_encoding?
274 282 str = str.encode("US-ASCII", :invalid => :replace,
275 283 :undef => :replace, :replace => '?').encode("UTF-8")
276 284 end
277 285 end
278 286 elsif RUBY_PLATFORM == 'java'
279 287 begin
280 288 ic = Iconv.new('UTF-8', enc)
281 289 str = ic.iconv(str)
282 290 rescue
283 291 str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
284 292 end
285 293 else
286 294 ic = Iconv.new('UTF-8', enc)
287 295 txtar = ""
288 296 begin
289 297 txtar += ic.iconv(str)
290 298 rescue Iconv::IllegalSequence
291 299 txtar += $!.success
292 300 str = '?' + $!.failed[1,$!.failed.length]
293 301 retry
294 302 rescue
295 303 txtar += $!.success
296 304 end
297 305 str = txtar
298 306 end
299 307 str
300 308 end
301 309 end
General Comments 0
You need to be logged in to leave comments. Login now