##// END OF EJS Templates
Fixes error with CVS+Postgresql and non-UTF8 commit logs (#917, #1659)....
Jean-Philippe Lang -
r1767:d611339baaf3
parent child
Show More
@@ -1,151 +1,156
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 '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 has_many :changes, :dependent => :delete_all
22 has_many :changes, :dependent => :delete_all
23 has_and_belongs_to_many :issues
23 has_and_belongs_to_many :issues
24
24
25 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
25 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
26 :description => :comments,
26 :description => :comments,
27 :datetime => :committed_on,
27 :datetime => :committed_on,
28 :author => :committer,
28 :author => :committer,
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
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 :find_options => {:include => {:repository => :project}}
37 :find_options => {:include => {:repository => :project}}
38
38
39 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
39 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
40 validates_uniqueness_of :revision, :scope => :repository_id
40 validates_uniqueness_of :revision, :scope => :repository_id
41 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
41 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
42
42
43 def revision=(r)
43 def revision=(r)
44 write_attribute :revision, (r.nil? ? nil : r.to_s)
44 write_attribute :revision, (r.nil? ? nil : r.to_s)
45 end
45 end
46
46
47 def comments=(comment)
47 def comments=(comment)
48 write_attribute(:comments, to_utf8(comment.to_s.strip))
48 write_attribute(:comments, Changeset.normalize_comments(comment))
49 end
49 end
50
50
51 def committed_on=(date)
51 def committed_on=(date)
52 self.commit_date = date
52 self.commit_date = date
53 super
53 super
54 end
54 end
55
55
56 def project
56 def project
57 repository.project
57 repository.project
58 end
58 end
59
59
60 def after_create
60 def after_create
61 scan_comment_for_issue_ids
61 scan_comment_for_issue_ids
62 end
62 end
63 require 'pp'
63 require 'pp'
64
64
65 def scan_comment_for_issue_ids
65 def scan_comment_for_issue_ids
66 return if comments.blank?
66 return if comments.blank?
67 # keywords used to reference issues
67 # keywords used to reference issues
68 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
68 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
69 # keywords used to fix issues
69 # keywords used to fix issues
70 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
70 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
71 # status and optional done ratio applied
71 # status and optional done ratio applied
72 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
72 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
73 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
73 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
74
74
75 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
75 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
76 return if kw_regexp.blank?
76 return if kw_regexp.blank?
77
77
78 referenced_issues = []
78 referenced_issues = []
79
79
80 if ref_keywords.delete('*')
80 if ref_keywords.delete('*')
81 # find any issue ID in the comments
81 # find any issue ID in the comments
82 target_issue_ids = []
82 target_issue_ids = []
83 comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
83 comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
84 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
84 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
85 end
85 end
86
86
87 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
87 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
88 action = match[0]
88 action = match[0]
89 target_issue_ids = match[1].scan(/\d+/)
89 target_issue_ids = match[1].scan(/\d+/)
90 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
90 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
91 if fix_status && fix_keywords.include?(action.downcase)
91 if fix_status && fix_keywords.include?(action.downcase)
92 # update status of issues
92 # update status of issues
93 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
93 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
94 target_issues.each do |issue|
94 target_issues.each do |issue|
95 # the issue may have been updated by the closure of another one (eg. duplicate)
95 # the issue may have been updated by the closure of another one (eg. duplicate)
96 issue.reload
96 issue.reload
97 # don't change the status is the issue is closed
97 # don't change the status is the issue is closed
98 next if issue.status.is_closed?
98 next if issue.status.is_closed?
99 user = committer_user || User.anonymous
99 user = committer_user || User.anonymous
100 csettext = "r#{self.revision}"
100 csettext = "r#{self.revision}"
101 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
101 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
102 csettext = "commit:\"#{self.scmid}\""
102 csettext = "commit:\"#{self.scmid}\""
103 end
103 end
104 journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
104 journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
105 issue.status = fix_status
105 issue.status = fix_status
106 issue.done_ratio = done_ratio if done_ratio
106 issue.done_ratio = done_ratio if done_ratio
107 issue.save
107 issue.save
108 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
108 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
109 end
109 end
110 end
110 end
111 referenced_issues += target_issues
111 referenced_issues += target_issues
112 end
112 end
113
113
114 self.issues = referenced_issues.uniq
114 self.issues = referenced_issues.uniq
115 end
115 end
116
116
117 # Returns the Redmine User corresponding to the committer
117 # Returns the Redmine User corresponding to the committer
118 def committer_user
118 def committer_user
119 if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
119 if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
120 username, email = $1.strip, $3
120 username, email = $1.strip, $3
121 u = User.find_by_login(username)
121 u = User.find_by_login(username)
122 u ||= User.find_by_mail(email) unless email.blank?
122 u ||= User.find_by_mail(email) unless email.blank?
123 u
123 u
124 end
124 end
125 end
125 end
126
126
127 # Returns the previous changeset
127 # Returns the previous changeset
128 def previous
128 def previous
129 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
129 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
130 end
130 end
131
131
132 # Returns the next changeset
132 # Returns the next changeset
133 def next
133 def next
134 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
134 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
135 end
135 end
136
136
137 # Strips and reencodes a commit log before insertion into the database
138 def self.normalize_comments(str)
139 to_utf8(str.to_s.strip)
140 end
141
137 private
142 private
138
143
139 def to_utf8(str)
144 def self.to_utf8(str)
140 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
145 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
141 encoding = Setting.commit_logs_encoding.to_s.strip
146 encoding = Setting.commit_logs_encoding.to_s.strip
142 unless encoding.blank? || encoding == 'UTF-8'
147 unless encoding.blank? || encoding == 'UTF-8'
143 begin
148 begin
144 return Iconv.conv('UTF-8', encoding, str)
149 return Iconv.conv('UTF-8', encoding, str)
145 rescue Iconv::Failure
150 rescue Iconv::Failure
146 # do nothing here
151 # do nothing here
147 end
152 end
148 end
153 end
149 str
154 str
150 end
155 end
151 end
156 end
@@ -1,161 +1,161
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
22 validates_presence_of :url, :root_url
23
23
24 def scm_adapter
24 def scm_adapter
25 Redmine::Scm::Adapters::CvsAdapter
25 Redmine::Scm::Adapters::CvsAdapter
26 end
26 end
27
27
28 def self.scm_name
28 def self.scm_name
29 'CVS'
29 'CVS'
30 end
30 end
31
31
32 def entry(path=nil, identifier=nil)
32 def entry(path=nil, identifier=nil)
33 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
33 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
34 scm.entry(path, rev.nil? ? nil : rev.committed_on)
34 scm.entry(path, rev.nil? ? nil : rev.committed_on)
35 end
35 end
36
36
37 def entries(path=nil, identifier=nil)
37 def entries(path=nil, identifier=nil)
38 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
38 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
39 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
39 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
40 if entries
40 if entries
41 entries.each() do |entry|
41 entries.each() do |entry|
42 unless entry.lastrev.nil? || entry.lastrev.identifier
42 unless entry.lastrev.nil? || entry.lastrev.identifier
43 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
43 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
44 if change
44 if change
45 entry.lastrev.identifier=change.changeset.revision
45 entry.lastrev.identifier=change.changeset.revision
46 entry.lastrev.author=change.changeset.committer
46 entry.lastrev.author=change.changeset.committer
47 entry.lastrev.revision=change.revision
47 entry.lastrev.revision=change.revision
48 entry.lastrev.branch=change.branch
48 entry.lastrev.branch=change.branch
49 end
49 end
50 end
50 end
51 end
51 end
52 end
52 end
53 entries
53 entries
54 end
54 end
55
55
56 def cat(path, identifier=nil)
56 def cat(path, identifier=nil)
57 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
57 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
58 scm.cat(path, rev.nil? ? nil : rev.committed_on)
58 scm.cat(path, rev.nil? ? nil : rev.committed_on)
59 end
59 end
60
60
61 def diff(path, rev, rev_to)
61 def diff(path, rev, rev_to)
62 #convert rev to revision. CVS can't handle changesets here
62 #convert rev to revision. CVS can't handle changesets here
63 diff=[]
63 diff=[]
64 changeset_from=changesets.find_by_revision(rev)
64 changeset_from=changesets.find_by_revision(rev)
65 if rev_to.to_i > 0
65 if rev_to.to_i > 0
66 changeset_to=changesets.find_by_revision(rev_to)
66 changeset_to=changesets.find_by_revision(rev_to)
67 end
67 end
68 changeset_from.changes.each() do |change_from|
68 changeset_from.changes.each() do |change_from|
69
69
70 revision_from=nil
70 revision_from=nil
71 revision_to=nil
71 revision_to=nil
72
72
73 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
73 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
74
74
75 if revision_from
75 if revision_from
76 if changeset_to
76 if changeset_to
77 changeset_to.changes.each() do |change_to|
77 changeset_to.changes.each() do |change_to|
78 revision_to=change_to.revision if change_to.path==change_from.path
78 revision_to=change_to.revision if change_to.path==change_from.path
79 end
79 end
80 end
80 end
81 unless revision_to
81 unless revision_to
82 revision_to=scm.get_previous_revision(revision_from)
82 revision_to=scm.get_previous_revision(revision_from)
83 end
83 end
84 file_diff = scm.diff(change_from.path, revision_from, revision_to)
84 file_diff = scm.diff(change_from.path, revision_from, revision_to)
85 diff = diff + file_diff unless file_diff.nil?
85 diff = diff + file_diff unless file_diff.nil?
86 end
86 end
87 end
87 end
88 return diff
88 return diff
89 end
89 end
90
90
91 def fetch_changesets
91 def fetch_changesets
92 # some nifty bits to introduce a commit-id with cvs
92 # some nifty bits to introduce a commit-id with cvs
93 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
93 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
94 # we now take a guess using the author, the commitlog and the commit-date.
94 # we now take a guess using the author, the commitlog and the commit-date.
95
95
96 # last one is the next step to take. the commit-date is not equal for all
96 # last one is the next step to take. the commit-date is not equal for all
97 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
97 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
98 # we use a small delta here, to merge all changes belonging to _one_ changeset
98 # we use a small delta here, to merge all changes belonging to _one_ changeset
99 time_delta=10.seconds
99 time_delta=10.seconds
100
100
101 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
101 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
102 transaction do
102 transaction do
103 tmp_rev_num = 1
103 tmp_rev_num = 1
104 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
104 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
105 # only add the change to the database, if it doen't exists. the cvs log
105 # only add the change to the database, if it doen't exists. the cvs log
106 # is not exclusive at all.
106 # is not exclusive at all.
107 unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
107 unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
108 revision
108 revision
109 cs = changesets.find(:first, :conditions=>{
109 cs = changesets.find(:first, :conditions=>{
110 :committed_on=>revision.time-time_delta..revision.time+time_delta,
110 :committed_on=>revision.time-time_delta..revision.time+time_delta,
111 :committer=>revision.author,
111 :committer=>revision.author,
112 :comments=>revision.message
112 :comments=>Changeset.normalize_comments(revision.message)
113 })
113 })
114
114
115 # create a new changeset....
115 # create a new changeset....
116 unless cs
116 unless cs
117 # we use a temporaray revision number here (just for inserting)
117 # we use a temporaray revision number here (just for inserting)
118 # later on, we calculate a continous positive number
118 # later on, we calculate a continous positive number
119 latest = changesets.find(:first, :order => 'id DESC')
119 latest = changesets.find(:first, :order => 'id DESC')
120 cs = Changeset.create(:repository => self,
120 cs = Changeset.create(:repository => self,
121 :revision => "_#{tmp_rev_num}",
121 :revision => "_#{tmp_rev_num}",
122 :committer => revision.author,
122 :committer => revision.author,
123 :committed_on => revision.time,
123 :committed_on => revision.time,
124 :comments => revision.message)
124 :comments => revision.message)
125 tmp_rev_num += 1
125 tmp_rev_num += 1
126 end
126 end
127
127
128 #convert CVS-File-States to internal Action-abbrevations
128 #convert CVS-File-States to internal Action-abbrevations
129 #default action is (M)odified
129 #default action is (M)odified
130 action="M"
130 action="M"
131 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
131 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
132 action="A" #add-action always at first revision (= 1.1)
132 action="A" #add-action always at first revision (= 1.1)
133 elsif revision.paths[0][:action]=="dead"
133 elsif revision.paths[0][:action]=="dead"
134 action="D" #dead-state is similar to Delete
134 action="D" #dead-state is similar to Delete
135 end
135 end
136
136
137 Change.create(:changeset => cs,
137 Change.create(:changeset => cs,
138 :action => action,
138 :action => action,
139 :path => scm.with_leading_slash(revision.paths[0][:path]),
139 :path => scm.with_leading_slash(revision.paths[0][:path]),
140 :revision => revision.paths[0][:revision],
140 :revision => revision.paths[0][:revision],
141 :branch => revision.paths[0][:branch]
141 :branch => revision.paths[0][:branch]
142 )
142 )
143 end
143 end
144 end
144 end
145
145
146 # Renumber new changesets in chronological order
146 # Renumber new changesets in chronological order
147 changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
147 changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
148 changeset.update_attribute :revision, next_revision_number
148 changeset.update_attribute :revision, next_revision_number
149 end
149 end
150 end # transaction
150 end # transaction
151 end
151 end
152
152
153 private
153 private
154
154
155 # Returns the next revision number to assign to a CVS changeset
155 # Returns the next revision number to assign to a CVS changeset
156 def next_revision_number
156 def next_revision_number
157 # Need to retrieve existing revision numbers to sort them as integers
157 # Need to retrieve existing revision numbers to sort them as integers
158 @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
158 @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
159 @current_revision_number += 1
159 @current_revision_number += 1
160 end
160 end
161 end
161 end
General Comments 0
You need to be logged in to leave comments. Login now