##// END OF EJS Templates
scm: cvs: fix parsing revisions if author is not ASCII....
Toshi MARUYAMA -
r5335:bebf8247a7f8
parent child
Show More
@@ -1,293 +1,291
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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
25
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 :description => :long_comments,
27 :description => :long_comments,
28 :datetime => :committed_on,
28 :datetime => :committed_on,
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
30
30
31 acts_as_searchable :columns => 'comments',
31 acts_as_searchable :columns => 'comments',
32 :include => {:repository => :project},
32 :include => {:repository => :project},
33 :project_key => "#{Repository.table_name}.project_id",
33 :project_key => "#{Repository.table_name}.project_id",
34 :date_column => 'committed_on'
34 :date_column => 'committed_on'
35
35
36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 :author_key => :user_id,
37 :author_key => :user_id,
38 :find_options => {:include => [:user, {:repository => :project}]}
38 :find_options => {:include => [:user, {:repository => :project}]}
39
39
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 validates_uniqueness_of :revision, :scope => :repository_id
41 validates_uniqueness_of :revision, :scope => :repository_id
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43
43
44 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
44 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
45 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
45 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
46
46
47 def revision=(r)
47 def revision=(r)
48 write_attribute :revision, (r.nil? ? nil : r.to_s)
48 write_attribute :revision, (r.nil? ? nil : r.to_s)
49 end
49 end
50
50
51 # Returns the identifier of this changeset; depending on repository backends
51 # Returns the identifier of this changeset; depending on repository backends
52 def identifier
52 def identifier
53 if repository.class.respond_to? :changeset_identifier
53 if repository.class.respond_to? :changeset_identifier
54 repository.class.changeset_identifier self
54 repository.class.changeset_identifier self
55 else
55 else
56 revision.to_s
56 revision.to_s
57 end
57 end
58 end
58 end
59
59
60 def committed_on=(date)
60 def committed_on=(date)
61 self.commit_date = date
61 self.commit_date = date
62 super
62 super
63 end
63 end
64
64
65 # Returns the readable identifier
65 # Returns the readable identifier
66 def format_identifier
66 def format_identifier
67 if repository.class.respond_to? :format_changeset_identifier
67 if repository.class.respond_to? :format_changeset_identifier
68 repository.class.format_changeset_identifier self
68 repository.class.format_changeset_identifier self
69 else
69 else
70 identifier
70 identifier
71 end
71 end
72 end
72 end
73
73
74 def project
74 def project
75 repository.project
75 repository.project
76 end
76 end
77
77
78 def author
78 def author
79 user || committer.to_s.split('<').first
79 user || committer.to_s.split('<').first
80 end
80 end
81
81
82 def before_create
82 def before_create
83 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
83 self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
84 self.comments = self.class.normalize_comments(
84 self.comments = self.class.normalize_comments(
85 self.comments, repository.repo_log_encoding)
85 self.comments, repository.repo_log_encoding)
86 self.user = repository.find_committer_user(self.committer)
86 self.user = repository.find_committer_user(self.committer)
87 end
87 end
88
88
89 def after_create
89 def after_create
90 scan_comment_for_issue_ids
90 scan_comment_for_issue_ids
91 end
91 end
92
92
93 TIMELOG_RE = /
93 TIMELOG_RE = /
94 (
94 (
95 ((\d+)(h|hours?))((\d+)(m|min)?)?
95 ((\d+)(h|hours?))((\d+)(m|min)?)?
96 |
96 |
97 ((\d+)(h|hours?|m|min))
97 ((\d+)(h|hours?|m|min))
98 |
98 |
99 (\d+):(\d+)
99 (\d+):(\d+)
100 |
100 |
101 (\d+([\.,]\d+)?)h?
101 (\d+([\.,]\d+)?)h?
102 )
102 )
103 /x
103 /x
104
104
105 def scan_comment_for_issue_ids
105 def scan_comment_for_issue_ids
106 return if comments.blank?
106 return if comments.blank?
107 # keywords used to reference issues
107 # keywords used to reference issues
108 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
108 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
109 ref_keywords_any = ref_keywords.delete('*')
109 ref_keywords_any = ref_keywords.delete('*')
110 # keywords used to fix issues
110 # keywords used to fix issues
111 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
111 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
112
112
113 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
113 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
114
114
115 referenced_issues = []
115 referenced_issues = []
116
116
117 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
117 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
118 action, refs = match[2], match[3]
118 action, refs = match[2], match[3]
119 next unless action.present? || ref_keywords_any
119 next unless action.present? || ref_keywords_any
120
120
121 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
121 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
122 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
122 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
123 if issue
123 if issue
124 referenced_issues << issue
124 referenced_issues << issue
125 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
125 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
126 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
126 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
127 end
127 end
128 end
128 end
129 end
129 end
130
130
131 referenced_issues.uniq!
131 referenced_issues.uniq!
132 self.issues = referenced_issues unless referenced_issues.empty?
132 self.issues = referenced_issues unless referenced_issues.empty?
133 end
133 end
134
134
135 def short_comments
135 def short_comments
136 @short_comments || split_comments.first
136 @short_comments || split_comments.first
137 end
137 end
138
138
139 def long_comments
139 def long_comments
140 @long_comments || split_comments.last
140 @long_comments || split_comments.last
141 end
141 end
142
142
143 def text_tag
143 def text_tag
144 if scmid?
144 if scmid?
145 "commit:#{scmid}"
145 "commit:#{scmid}"
146 else
146 else
147 "r#{revision}"
147 "r#{revision}"
148 end
148 end
149 end
149 end
150
150
151 # Returns the previous changeset
151 # Returns the previous changeset
152 def previous
152 def previous
153 @previous ||= Changeset.find(:first,
153 @previous ||= Changeset.find(:first,
154 :conditions => ['id < ? AND repository_id = ?',
154 :conditions => ['id < ? AND repository_id = ?',
155 self.id, self.repository_id],
155 self.id, self.repository_id],
156 :order => 'id DESC')
156 :order => 'id DESC')
157 end
157 end
158
158
159 # Returns the next changeset
159 # Returns the next changeset
160 def next
160 def next
161 @next ||= Changeset.find(:first,
161 @next ||= Changeset.find(:first,
162 :conditions => ['id > ? AND repository_id = ?',
162 :conditions => ['id > ? AND repository_id = ?',
163 self.id, self.repository_id],
163 self.id, self.repository_id],
164 :order => 'id ASC')
164 :order => 'id ASC')
165 end
165 end
166
166
167 # Creates a new Change from it's common parameters
167 # Creates a new Change from it's common parameters
168 def create_change(change)
168 def create_change(change)
169 Change.create(:changeset => self,
169 Change.create(:changeset => self,
170 :action => change[:action],
170 :action => change[:action],
171 :path => change[:path],
171 :path => change[:path],
172 :from_path => change[:from_path],
172 :from_path => change[:from_path],
173 :from_revision => change[:from_revision])
173 :from_revision => change[:from_revision])
174 end
174 end
175
175
176 private
176 private
177
177
178 # Finds an issue that can be referenced by the commit message
178 # Finds an issue that can be referenced by the commit message
179 # i.e. an issue that belong to the repository project, a subproject or a parent project
179 # i.e. an issue that belong to the repository project, a subproject or a parent project
180 def find_referenced_issue_by_id(id)
180 def find_referenced_issue_by_id(id)
181 return nil if id.blank?
181 return nil if id.blank?
182 issue = Issue.find_by_id(id.to_i, :include => :project)
182 issue = Issue.find_by_id(id.to_i, :include => :project)
183 if issue
183 if issue
184 unless issue.project &&
184 unless issue.project &&
185 (project == issue.project || project.is_ancestor_of?(issue.project) ||
185 (project == issue.project || project.is_ancestor_of?(issue.project) ||
186 project.is_descendant_of?(issue.project))
186 project.is_descendant_of?(issue.project))
187 issue = nil
187 issue = nil
188 end
188 end
189 end
189 end
190 issue
190 issue
191 end
191 end
192
192
193 def fix_issue(issue)
193 def fix_issue(issue)
194 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
194 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
195 if status.nil?
195 if status.nil?
196 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
196 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
197 return issue
197 return issue
198 end
198 end
199
199
200 # the issue may have been updated by the closure of another one (eg. duplicate)
200 # the issue may have been updated by the closure of another one (eg. duplicate)
201 issue.reload
201 issue.reload
202 # don't change the status is the issue is closed
202 # don't change the status is the issue is closed
203 return if issue.status && issue.status.is_closed?
203 return if issue.status && issue.status.is_closed?
204
204
205 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
205 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
206 issue.status = status
206 issue.status = status
207 unless Setting.commit_fix_done_ratio.blank?
207 unless Setting.commit_fix_done_ratio.blank?
208 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
208 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
209 end
209 end
210 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
210 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
211 { :changeset => self, :issue => issue })
211 { :changeset => self, :issue => issue })
212 unless issue.save
212 unless issue.save
213 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
213 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
214 end
214 end
215 issue
215 issue
216 end
216 end
217
217
218 def log_time(issue, hours)
218 def log_time(issue, hours)
219 time_entry = TimeEntry.new(
219 time_entry = TimeEntry.new(
220 :user => user,
220 :user => user,
221 :hours => hours,
221 :hours => hours,
222 :issue => issue,
222 :issue => issue,
223 :spent_on => commit_date,
223 :spent_on => commit_date,
224 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
224 :comments => l(:text_time_logged_by_changeset, :value => text_tag,
225 :locale => Setting.default_language)
225 :locale => Setting.default_language)
226 )
226 )
227 time_entry.activity = log_time_activity unless log_time_activity.nil?
227 time_entry.activity = log_time_activity unless log_time_activity.nil?
228
228
229 unless time_entry.save
229 unless time_entry.save
230 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
230 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
231 end
231 end
232 time_entry
232 time_entry
233 end
233 end
234
234
235 def log_time_activity
235 def log_time_activity
236 if Setting.commit_logtime_activity_id.to_i > 0
236 if Setting.commit_logtime_activity_id.to_i > 0
237 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
237 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
238 end
238 end
239 end
239 end
240
240
241 def split_comments
241 def split_comments
242 comments =~ /\A(.+?)\r?\n(.*)$/m
242 comments =~ /\A(.+?)\r?\n(.*)$/m
243 @short_comments = $1 || comments
243 @short_comments = $1 || comments
244 @long_comments = $2.to_s.strip
244 @long_comments = $2.to_s.strip
245 return @short_comments, @long_comments
245 return @short_comments, @long_comments
246 end
246 end
247
247
248 public
248 public
249
249
250 # Strips and reencodes a commit log before insertion into the database
250 # Strips and reencodes a commit log before insertion into the database
251 def self.normalize_comments(str, encoding)
251 def self.normalize_comments(str, encoding)
252 Changeset.to_utf8(str.to_s.strip, encoding)
252 Changeset.to_utf8(str.to_s.strip, encoding)
253 end
253 end
254
254
255 private
256
257 def self.to_utf8(str, encoding)
255 def self.to_utf8(str, encoding)
258 return str if str.nil?
256 return str if str.nil?
259 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
257 str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
260 if str.empty?
258 if str.empty?
261 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
259 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
262 return str
260 return str
263 end
261 end
264 enc = encoding.blank? ? "UTF-8" : encoding
262 enc = encoding.blank? ? "UTF-8" : encoding
265 if str.respond_to?(:force_encoding)
263 if str.respond_to?(:force_encoding)
266 if enc.upcase != "UTF-8"
264 if enc.upcase != "UTF-8"
267 str.force_encoding(enc)
265 str.force_encoding(enc)
268 str = str.encode("UTF-8", :invalid => :replace,
266 str = str.encode("UTF-8", :invalid => :replace,
269 :undef => :replace, :replace => '?')
267 :undef => :replace, :replace => '?')
270 else
268 else
271 str.force_encoding("UTF-8")
269 str.force_encoding("UTF-8")
272 if ! str.valid_encoding?
270 if ! str.valid_encoding?
273 str = str.encode("US-ASCII", :invalid => :replace,
271 str = str.encode("US-ASCII", :invalid => :replace,
274 :undef => :replace, :replace => '?').encode("UTF-8")
272 :undef => :replace, :replace => '?').encode("UTF-8")
275 end
273 end
276 end
274 end
277 else
275 else
278 ic = Iconv.new('UTF-8', enc)
276 ic = Iconv.new('UTF-8', enc)
279 txtar = ""
277 txtar = ""
280 begin
278 begin
281 txtar += ic.iconv(str)
279 txtar += ic.iconv(str)
282 rescue Iconv::IllegalSequence
280 rescue Iconv::IllegalSequence
283 txtar += $!.success
281 txtar += $!.success
284 str = '?' + $!.failed[1,$!.failed.length]
282 str = '?' + $!.failed[1,$!.failed.length]
285 retry
283 retry
286 rescue
284 rescue
287 txtar += $!.success
285 txtar += $!.success
288 end
286 end
289 str = txtar
287 str = txtar
290 end
288 end
291 str
289 str
292 end
290 end
293 end
291 end
@@ -1,203 +1,204
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'redmine/scm/adapters/cvs_adapter'
18 require 'redmine/scm/adapters/cvs_adapter'
19 require 'digest/sha1'
19 require 'digest/sha1'
20
20
21 class Repository::Cvs < Repository
21 class Repository::Cvs < Repository
22 validates_presence_of :url, :root_url, :log_encoding
22 validates_presence_of :url, :root_url, :log_encoding
23
23
24 ATTRIBUTE_KEY_NAMES = {
24 ATTRIBUTE_KEY_NAMES = {
25 "url" => "CVSROOT",
25 "url" => "CVSROOT",
26 "root_url" => "Module",
26 "root_url" => "Module",
27 "log_encoding" => "Commit messages encoding",
27 "log_encoding" => "Commit messages encoding",
28 }
28 }
29 def self.human_attribute_name(attribute_key_name)
29 def self.human_attribute_name(attribute_key_name)
30 ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
30 ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
31 end
31 end
32
32
33 def self.scm_adapter_class
33 def self.scm_adapter_class
34 Redmine::Scm::Adapters::CvsAdapter
34 Redmine::Scm::Adapters::CvsAdapter
35 end
35 end
36
36
37 def self.scm_name
37 def self.scm_name
38 'CVS'
38 'CVS'
39 end
39 end
40
40
41 def entry(path=nil, identifier=nil)
41 def entry(path=nil, identifier=nil)
42 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
42 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
43 scm.entry(path, rev.nil? ? nil : rev.committed_on)
43 scm.entry(path, rev.nil? ? nil : rev.committed_on)
44 end
44 end
45
45
46 def entries(path=nil, identifier=nil)
46 def entries(path=nil, identifier=nil)
47 rev = nil
47 rev = nil
48 if ! identifier.nil?
48 if ! identifier.nil?
49 rev = changesets.find_by_revision(identifier)
49 rev = changesets.find_by_revision(identifier)
50 return nil if rev.nil?
50 return nil if rev.nil?
51 end
51 end
52 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
52 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
53 if entries
53 if entries
54 entries.each() do |entry|
54 entries.each() do |entry|
55 if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? )
55 if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? )
56 change=changes.find_by_revision_and_path(
56 change=changes.find_by_revision_and_path(
57 entry.lastrev.revision,
57 entry.lastrev.revision,
58 scm.with_leading_slash(entry.path) )
58 scm.with_leading_slash(entry.path) )
59 if change
59 if change
60 entry.lastrev.identifier = change.changeset.revision
60 entry.lastrev.identifier = change.changeset.revision
61 entry.lastrev.revision = change.changeset.revision
61 entry.lastrev.revision = change.changeset.revision
62 entry.lastrev.author = change.changeset.committer
62 entry.lastrev.author = change.changeset.committer
63 # entry.lastrev.branch = change.branch
63 # entry.lastrev.branch = change.branch
64 end
64 end
65 end
65 end
66 end
66 end
67 end
67 end
68 entries
68 entries
69 end
69 end
70
70
71 def cat(path, identifier=nil)
71 def cat(path, identifier=nil)
72 rev = nil
72 rev = nil
73 if ! identifier.nil?
73 if ! identifier.nil?
74 rev = changesets.find_by_revision(identifier)
74 rev = changesets.find_by_revision(identifier)
75 return nil if rev.nil?
75 return nil if rev.nil?
76 end
76 end
77 scm.cat(path, rev.nil? ? nil : rev.committed_on)
77 scm.cat(path, rev.nil? ? nil : rev.committed_on)
78 end
78 end
79
79
80 def annotate(path, identifier=nil)
80 def annotate(path, identifier=nil)
81 rev = nil
81 rev = nil
82 if ! identifier.nil?
82 if ! identifier.nil?
83 rev = changesets.find_by_revision(identifier)
83 rev = changesets.find_by_revision(identifier)
84 return nil if rev.nil?
84 return nil if rev.nil?
85 end
85 end
86 scm.annotate(path, rev.nil? ? nil : rev.committed_on)
86 scm.annotate(path, rev.nil? ? nil : rev.committed_on)
87 end
87 end
88
88
89 def diff(path, rev, rev_to)
89 def diff(path, rev, rev_to)
90 # convert rev to revision. CVS can't handle changesets here
90 # convert rev to revision. CVS can't handle changesets here
91 diff=[]
91 diff=[]
92 changeset_from = changesets.find_by_revision(rev)
92 changeset_from = changesets.find_by_revision(rev)
93 if rev_to.to_i > 0
93 if rev_to.to_i > 0
94 changeset_to = changesets.find_by_revision(rev_to)
94 changeset_to = changesets.find_by_revision(rev_to)
95 end
95 end
96 changeset_from.changes.each() do |change_from|
96 changeset_from.changes.each() do |change_from|
97 revision_from = nil
97 revision_from = nil
98 revision_to = nil
98 revision_to = nil
99 if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
99 if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
100 revision_from = change_from.revision
100 revision_from = change_from.revision
101 end
101 end
102 if revision_from
102 if revision_from
103 if changeset_to
103 if changeset_to
104 changeset_to.changes.each() do |change_to|
104 changeset_to.changes.each() do |change_to|
105 revision_to=change_to.revision if change_to.path==change_from.path
105 revision_to=change_to.revision if change_to.path==change_from.path
106 end
106 end
107 end
107 end
108 unless revision_to
108 unless revision_to
109 revision_to=scm.get_previous_revision(revision_from)
109 revision_to=scm.get_previous_revision(revision_from)
110 end
110 end
111 file_diff = scm.diff(change_from.path, revision_from, revision_to)
111 file_diff = scm.diff(change_from.path, revision_from, revision_to)
112 diff = diff + file_diff unless file_diff.nil?
112 diff = diff + file_diff unless file_diff.nil?
113 end
113 end
114 end
114 end
115 return diff
115 return diff
116 end
116 end
117
117
118 def fetch_changesets
118 def fetch_changesets
119 # some nifty bits to introduce a commit-id with cvs
119 # some nifty bits to introduce a commit-id with cvs
120 # natively cvs doesn't provide any kind of changesets,
120 # natively cvs doesn't provide any kind of changesets,
121 # there is only a revision per file.
121 # there is only a revision per file.
122 # we now take a guess using the author, the commitlog and the commit-date.
122 # we now take a guess using the author, the commitlog and the commit-date.
123
123
124 # last one is the next step to take. the commit-date is not equal for all
124 # last one is the next step to take. the commit-date is not equal for all
125 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
125 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
126 # we use a small delta here, to merge all changes belonging to _one_ changeset
126 # we use a small delta here, to merge all changes belonging to _one_ changeset
127 time_delta = 10.seconds
127 time_delta = 10.seconds
128 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
128 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
129 transaction do
129 transaction do
130 tmp_rev_num = 1
130 tmp_rev_num = 1
131 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
131 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
132 # only add the change to the database, if it doen't exists. the cvs log
132 # only add the change to the database, if it doen't exists. the cvs log
133 # is not exclusive at all.
133 # is not exclusive at all.
134 tmp_time = revision.time.clone
134 tmp_time = revision.time.clone
135 unless changes.find_by_path_and_revision(
135 unless changes.find_by_path_and_revision(
136 scm.with_leading_slash(revision.paths[0][:path]),
136 scm.with_leading_slash(revision.paths[0][:path]),
137 revision.paths[0][:revision]
137 revision.paths[0][:revision]
138 )
138 )
139 cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
139 cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
140 author_utf8 = Changeset.to_utf8(revision.author, repo_log_encoding)
140 cs = changesets.find(
141 cs = changesets.find(
141 :first,
142 :first,
142 :conditions => {
143 :conditions => {
143 :committed_on => tmp_time - time_delta .. tmp_time + time_delta,
144 :committed_on => tmp_time - time_delta .. tmp_time + time_delta,
144 :committer => revision.author,
145 :committer => author_utf8,
145 :comments => cmt
146 :comments => cmt
146 }
147 }
147 )
148 )
148 # create a new changeset....
149 # create a new changeset....
149 unless cs
150 unless cs
150 # we use a temporaray revision number here (just for inserting)
151 # we use a temporaray revision number here (just for inserting)
151 # later on, we calculate a continous positive number
152 # later on, we calculate a continous positive number
152 tmp_time2 = tmp_time.clone.gmtime
153 tmp_time2 = tmp_time.clone.gmtime
153 branch = revision.paths[0][:branch]
154 branch = revision.paths[0][:branch]
154 scmid = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
155 scmid = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
155 cs = Changeset.create(:repository => self,
156 cs = Changeset.create(:repository => self,
156 :revision => "tmp#{tmp_rev_num}",
157 :revision => "tmp#{tmp_rev_num}",
157 :scmid => scmid,
158 :scmid => scmid,
158 :committer => revision.author,
159 :committer => revision.author,
159 :committed_on => tmp_time,
160 :committed_on => tmp_time,
160 :comments => revision.message)
161 :comments => revision.message)
161 tmp_rev_num += 1
162 tmp_rev_num += 1
162 end
163 end
163 # convert CVS-File-States to internal Action-abbrevations
164 # convert CVS-File-States to internal Action-abbrevations
164 # default action is (M)odified
165 # default action is (M)odified
165 action = "M"
166 action = "M"
166 if revision.paths[0][:action] == "Exp" && revision.paths[0][:revision] == "1.1"
167 if revision.paths[0][:action] == "Exp" && revision.paths[0][:revision] == "1.1"
167 action = "A" # add-action always at first revision (= 1.1)
168 action = "A" # add-action always at first revision (= 1.1)
168 elsif revision.paths[0][:action] == "dead"
169 elsif revision.paths[0][:action] == "dead"
169 action = "D" # dead-state is similar to Delete
170 action = "D" # dead-state is similar to Delete
170 end
171 end
171 Change.create(
172 Change.create(
172 :changeset => cs,
173 :changeset => cs,
173 :action => action,
174 :action => action,
174 :path => scm.with_leading_slash(revision.paths[0][:path]),
175 :path => scm.with_leading_slash(revision.paths[0][:path]),
175 :revision => revision.paths[0][:revision],
176 :revision => revision.paths[0][:revision],
176 :branch => revision.paths[0][:branch]
177 :branch => revision.paths[0][:branch]
177 )
178 )
178 end
179 end
179 end
180 end
180
181
181 # Renumber new changesets in chronological order
182 # Renumber new changesets in chronological order
182 changesets.find(
183 changesets.find(
183 :all,
184 :all,
184 :order => 'committed_on ASC, id ASC',
185 :order => 'committed_on ASC, id ASC',
185 :conditions => "revision LIKE 'tmp%'"
186 :conditions => "revision LIKE 'tmp%'"
186 ).each do |changeset|
187 ).each do |changeset|
187 changeset.update_attribute :revision, next_revision_number
188 changeset.update_attribute :revision, next_revision_number
188 end
189 end
189 end # transaction
190 end # transaction
190 @current_revision_number = nil
191 @current_revision_number = nil
191 end
192 end
192
193
193 private
194 private
194
195
195 # Returns the next revision number to assign to a CVS changeset
196 # Returns the next revision number to assign to a CVS changeset
196 def next_revision_number
197 def next_revision_number
197 # Need to retrieve existing revision numbers to sort them as integers
198 # Need to retrieve existing revision numbers to sort them as integers
198 sql = "SELECT revision FROM #{Changeset.table_name} "
199 sql = "SELECT revision FROM #{Changeset.table_name} "
199 sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
200 sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
200 @current_revision_number ||= (connection.select_values(sql).collect(&:to_i).max || 0)
201 @current_revision_number ||= (connection.select_values(sql).collect(&:to_i).max || 0)
201 @current_revision_number += 1
202 @current_revision_number += 1
202 end
203 end
203 end
204 end
General Comments 0
You need to be logged in to leave comments. Login now