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