##// END OF EJS Templates
scm: cvs: convert author encoding with log encoding setting....
Toshi MARUYAMA -
r5336:04a22fa757a5
parent child
Show More
@@ -1,204 +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, :log_encoding => repo_log_encoding) 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 author_utf8 = Changeset.to_utf8(revision.author, repo_log_encoding)
141 cs = changesets.find(
141 cs = changesets.find(
142 :first,
142 :first,
143 :conditions => {
143 :conditions => {
144 :committed_on => tmp_time - time_delta .. tmp_time + time_delta,
144 :committed_on => tmp_time - time_delta .. tmp_time + time_delta,
145 :committer => author_utf8,
145 :committer => author_utf8,
146 :comments => cmt
146 :comments => cmt
147 }
147 }
148 )
148 )
149 # create a new changeset....
149 # create a new changeset....
150 unless cs
150 unless cs
151 # we use a temporaray revision number here (just for inserting)
151 # we use a temporaray revision number here (just for inserting)
152 # later on, we calculate a continous positive number
152 # later on, we calculate a continous positive number
153 tmp_time2 = tmp_time.clone.gmtime
153 tmp_time2 = tmp_time.clone.gmtime
154 branch = revision.paths[0][:branch]
154 branch = revision.paths[0][:branch]
155 scmid = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
155 scmid = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
156 cs = Changeset.create(:repository => self,
156 cs = Changeset.create(:repository => self,
157 :revision => "tmp#{tmp_rev_num}",
157 :revision => "tmp#{tmp_rev_num}",
158 :scmid => scmid,
158 :scmid => scmid,
159 :committer => revision.author,
159 :committer => revision.author,
160 :committed_on => tmp_time,
160 :committed_on => tmp_time,
161 :comments => revision.message)
161 :comments => revision.message)
162 tmp_rev_num += 1
162 tmp_rev_num += 1
163 end
163 end
164 # convert CVS-File-States to internal Action-abbrevations
164 # convert CVS-File-States to internal Action-abbrevations
165 # default action is (M)odified
165 # default action is (M)odified
166 action = "M"
166 action = "M"
167 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"
168 action = "A" # add-action always at first revision (= 1.1)
168 action = "A" # add-action always at first revision (= 1.1)
169 elsif revision.paths[0][:action] == "dead"
169 elsif revision.paths[0][:action] == "dead"
170 action = "D" # dead-state is similar to Delete
170 action = "D" # dead-state is similar to Delete
171 end
171 end
172 Change.create(
172 Change.create(
173 :changeset => cs,
173 :changeset => cs,
174 :action => action,
174 :action => action,
175 :path => scm.with_leading_slash(revision.paths[0][:path]),
175 :path => scm.with_leading_slash(revision.paths[0][:path]),
176 :revision => revision.paths[0][:revision],
176 :revision => revision.paths[0][:revision],
177 :branch => revision.paths[0][:branch]
177 :branch => revision.paths[0][:branch]
178 )
178 )
179 end
179 end
180 end
180 end
181
181
182 # Renumber new changesets in chronological order
182 # Renumber new changesets in chronological order
183 changesets.find(
183 changesets.find(
184 :all,
184 :all,
185 :order => 'committed_on ASC, id ASC',
185 :order => 'committed_on ASC, id ASC',
186 :conditions => "revision LIKE 'tmp%'"
186 :conditions => "revision LIKE 'tmp%'"
187 ).each do |changeset|
187 ).each do |changeset|
188 changeset.update_attribute :revision, next_revision_number
188 changeset.update_attribute :revision, next_revision_number
189 end
189 end
190 end # transaction
190 end # transaction
191 @current_revision_number = nil
191 @current_revision_number = nil
192 end
192 end
193
193
194 private
194 private
195
195
196 # Returns the next revision number to assign to a CVS changeset
196 # Returns the next revision number to assign to a CVS changeset
197 def next_revision_number
197 def next_revision_number
198 # Need to retrieve existing revision numbers to sort them as integers
198 # Need to retrieve existing revision numbers to sort them as integers
199 sql = "SELECT revision FROM #{Changeset.table_name} "
199 sql = "SELECT revision FROM #{Changeset.table_name} "
200 sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
200 sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
201 @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)
202 @current_revision_number += 1
202 @current_revision_number += 1
203 end
203 end
204 end
204 end
@@ -1,448 +1,449
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/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class CvsAdapter < AbstractAdapter
23 class CvsAdapter < AbstractAdapter
24
24
25 # CVS executable name
25 # CVS executable name
26 CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
26 CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
27
27
28 # raised if scm command exited with error, e.g. unknown revision.
28 # raised if scm command exited with error, e.g. unknown revision.
29 class ScmCommandAborted < CommandFailed; end
29 class ScmCommandAborted < CommandFailed; end
30
30
31 class << self
31 class << self
32 def client_command
32 def client_command
33 @@bin ||= CVS_BIN
33 @@bin ||= CVS_BIN
34 end
34 end
35
35
36 def sq_bin
36 def sq_bin
37 @@sq_bin ||= shell_quote(CVS_BIN)
37 @@sq_bin ||= shell_quote(CVS_BIN)
38 end
38 end
39
39
40 def client_version
40 def client_version
41 @@client_version ||= (scm_command_version || [])
41 @@client_version ||= (scm_command_version || [])
42 end
42 end
43
43
44 def client_available
44 def client_available
45 client_version_above?([1, 12])
45 client_version_above?([1, 12])
46 end
46 end
47
47
48 def scm_command_version
48 def scm_command_version
49 scm_version = scm_version_from_command_line.dup
49 scm_version = scm_version_from_command_line.dup
50 if scm_version.respond_to?(:force_encoding)
50 if scm_version.respond_to?(:force_encoding)
51 scm_version.force_encoding('ASCII-8BIT')
51 scm_version.force_encoding('ASCII-8BIT')
52 end
52 end
53 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
53 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
54 m[2].scan(%r{\d+}).collect(&:to_i)
54 m[2].scan(%r{\d+}).collect(&:to_i)
55 end
55 end
56 end
56 end
57
57
58 def scm_version_from_command_line
58 def scm_version_from_command_line
59 shellout("#{sq_bin} --version") { |io| io.read }.to_s
59 shellout("#{sq_bin} --version") { |io| io.read }.to_s
60 end
60 end
61 end
61 end
62
62
63 # Guidelines for the input:
63 # Guidelines for the input:
64 # url -> the project-path, relative to the cvsroot (eg. module name)
64 # url -> the project-path, relative to the cvsroot (eg. module name)
65 # root_url -> the good old, sometimes damned, CVSROOT
65 # root_url -> the good old, sometimes damned, CVSROOT
66 # login -> unnecessary
66 # login -> unnecessary
67 # password -> unnecessary too
67 # password -> unnecessary too
68 def initialize(url, root_url=nil, login=nil, password=nil,
68 def initialize(url, root_url=nil, login=nil, password=nil,
69 path_encoding=nil)
69 path_encoding=nil)
70 @url = url
70 @url = url
71 # TODO: better Exception here (IllegalArgumentException)
71 # TODO: better Exception here (IllegalArgumentException)
72 raise CommandFailed if root_url.blank?
72 raise CommandFailed if root_url.blank?
73 @root_url = root_url
73 @root_url = root_url
74
74
75 # These are unused.
75 # These are unused.
76 @login = login if login && !login.empty?
76 @login = login if login && !login.empty?
77 @password = (password || "") if @login
77 @password = (password || "") if @login
78 end
78 end
79
79
80 def info
80 def info
81 logger.debug "<cvs> info"
81 logger.debug "<cvs> info"
82 Info.new({:root_url => @root_url, :lastrev => nil})
82 Info.new({:root_url => @root_url, :lastrev => nil})
83 end
83 end
84
84
85 def get_previous_revision(revision)
85 def get_previous_revision(revision)
86 CvsRevisionHelper.new(revision).prevRev
86 CvsRevisionHelper.new(revision).prevRev
87 end
87 end
88
88
89 # Returns an Entries collection
89 # Returns an Entries collection
90 # or nil if the given path doesn't exist in the repository
90 # or nil if the given path doesn't exist in the repository
91 # this method is used by the repository-browser (aka LIST)
91 # this method is used by the repository-browser (aka LIST)
92 def entries(path=nil, identifier=nil)
92 def entries(path=nil, identifier=nil)
93 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
93 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
94 entries = Entries.new
94 entries = Entries.new
95 cmd_args = %w|-q rls -e|
95 cmd_args = %w|-q rls -e|
96 cmd_args << "-D" << time_to_cvstime_rlog(identifier) if identifier
96 cmd_args << "-D" << time_to_cvstime_rlog(identifier) if identifier
97 cmd_args << path_with_proj(path)
97 cmd_args << path_with_proj(path)
98 scm_cmd(*cmd_args) do |io|
98 scm_cmd(*cmd_args) do |io|
99 io.each_line() do |line|
99 io.each_line() do |line|
100 fields = line.chop.split('/',-1)
100 fields = line.chop.split('/',-1)
101 logger.debug(">>InspectLine #{fields.inspect}")
101 logger.debug(">>InspectLine #{fields.inspect}")
102 if fields[0]!="D"
102 if fields[0]!="D"
103 time = nil
103 time = nil
104 # Thu Dec 13 16:27:22 2007
104 # Thu Dec 13 16:27:22 2007
105 time_l = fields[-3].split(' ')
105 time_l = fields[-3].split(' ')
106 if time_l.size == 5 && time_l[4].length == 4
106 if time_l.size == 5 && time_l[4].length == 4
107 begin
107 begin
108 time = Time.parse(
108 time = Time.parse(
109 "#{time_l[1]} #{time_l[2]} #{time_l[3]} GMT #{time_l[4]}")
109 "#{time_l[1]} #{time_l[2]} #{time_l[3]} GMT #{time_l[4]}")
110 rescue
110 rescue
111 end
111 end
112 end
112 end
113 entries << Entry.new(
113 entries << Entry.new(
114 {
114 {
115 :name => fields[-5],
115 :name => fields[-5],
116 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
116 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
117 :path => "#{path}/#{fields[-5]}",
117 :path => "#{path}/#{fields[-5]}",
118 :kind => 'file',
118 :kind => 'file',
119 :size => nil,
119 :size => nil,
120 :lastrev => Revision.new(
120 :lastrev => Revision.new(
121 {
121 {
122 :revision => fields[-4],
122 :revision => fields[-4],
123 :name => fields[-4],
123 :name => fields[-4],
124 :time => time,
124 :time => time,
125 :author => ''
125 :author => ''
126 })
126 })
127 })
127 })
128 else
128 else
129 entries << Entry.new(
129 entries << Entry.new(
130 {
130 {
131 :name => fields[1],
131 :name => fields[1],
132 :path => "#{path}/#{fields[1]}",
132 :path => "#{path}/#{fields[1]}",
133 :kind => 'dir',
133 :kind => 'dir',
134 :size => nil,
134 :size => nil,
135 :lastrev => nil
135 :lastrev => nil
136 })
136 })
137 end
137 end
138 end
138 end
139 end
139 end
140 entries.sort_by_name
140 entries.sort_by_name
141 rescue ScmCommandAborted
141 rescue ScmCommandAborted
142 nil
142 nil
143 end
143 end
144
144
145 STARTLOG="----------------------------"
145 STARTLOG="----------------------------"
146 ENDLOG ="============================================================================="
146 ENDLOG ="============================================================================="
147
147
148 # Returns all revisions found between identifier_from and identifier_to
148 # Returns all revisions found between identifier_from and identifier_to
149 # in the repository. both identifier have to be dates or nil.
149 # in the repository. both identifier have to be dates or nil.
150 # these method returns nothing but yield every result in block
150 # these method returns nothing but yield every result in block
151 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
151 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
152 logger.debug "<cvs> revisions path:" +
152 logger.debug "<cvs> revisions path:" +
153 "'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
153 "'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
154 cmd_args = %w|-q rlog|
154 cmd_args = %w|-q rlog|
155 cmd_args << "-d" << ">#{time_to_cvstime_rlog(identifier_from)}" if identifier_from
155 cmd_args << "-d" << ">#{time_to_cvstime_rlog(identifier_from)}" if identifier_from
156 cmd_args << path_with_proj(path)
156 cmd_args << path_with_proj(path)
157 scm_cmd(*cmd_args) do |io|
157 scm_cmd(*cmd_args) do |io|
158 state = "entry_start"
158 state = "entry_start"
159 commit_log = String.new
159 commit_log = String.new
160 revision = nil
160 revision = nil
161 date = nil
161 date = nil
162 author = nil
162 author = nil
163 entry_path = nil
163 entry_path = nil
164 entry_name = nil
164 entry_name = nil
165 file_state = nil
165 file_state = nil
166 branch_map = nil
166 branch_map = nil
167 io.each_line() do |line|
167 io.each_line() do |line|
168 if state != "revision" && /^#{ENDLOG}/ =~ line
168 if state != "revision" && /^#{ENDLOG}/ =~ line
169 commit_log = String.new
169 commit_log = String.new
170 revision = nil
170 revision = nil
171 state = "entry_start"
171 state = "entry_start"
172 end
172 end
173 if state == "entry_start"
173 if state == "entry_start"
174 branch_map = Hash.new
174 branch_map = Hash.new
175 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_proj(path))}(.+),v$/ =~ line
175 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_proj(path))}(.+),v$/ =~ line
176 entry_path = normalize_cvs_path($1)
176 entry_path = normalize_cvs_path($1)
177 entry_name = normalize_path(File.basename($1))
177 entry_name = normalize_path(File.basename($1))
178 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
178 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
179 elsif /^head: (.+)$/ =~ line
179 elsif /^head: (.+)$/ =~ line
180 entry_headRev = $1 #unless entry.nil?
180 entry_headRev = $1 #unless entry.nil?
181 elsif /^symbolic names:/ =~ line
181 elsif /^symbolic names:/ =~ line
182 state = "symbolic" #unless entry.nil?
182 state = "symbolic" #unless entry.nil?
183 elsif /^#{STARTLOG}/ =~ line
183 elsif /^#{STARTLOG}/ =~ line
184 commit_log = String.new
184 commit_log = String.new
185 state = "revision"
185 state = "revision"
186 end
186 end
187 next
187 next
188 elsif state == "symbolic"
188 elsif state == "symbolic"
189 if /^(.*):\s(.*)/ =~ (line.strip)
189 if /^(.*):\s(.*)/ =~ (line.strip)
190 branch_map[$1] = $2
190 branch_map[$1] = $2
191 else
191 else
192 state = "tags"
192 state = "tags"
193 next
193 next
194 end
194 end
195 elsif state == "tags"
195 elsif state == "tags"
196 if /^#{STARTLOG}/ =~ line
196 if /^#{STARTLOG}/ =~ line
197 commit_log = ""
197 commit_log = ""
198 state = "revision"
198 state = "revision"
199 elsif /^#{ENDLOG}/ =~ line
199 elsif /^#{ENDLOG}/ =~ line
200 state = "head"
200 state = "head"
201 end
201 end
202 next
202 next
203 elsif state == "revision"
203 elsif state == "revision"
204 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
204 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
205 if revision
205 if revision
206 revHelper = CvsRevisionHelper.new(revision)
206 revHelper = CvsRevisionHelper.new(revision)
207 revBranch = "HEAD"
207 revBranch = "HEAD"
208 branch_map.each() do |branch_name, branch_point|
208 branch_map.each() do |branch_name, branch_point|
209 if revHelper.is_in_branch_with_symbol(branch_point)
209 if revHelper.is_in_branch_with_symbol(branch_point)
210 revBranch = branch_name
210 revBranch = branch_name
211 end
211 end
212 end
212 end
213 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
213 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
214 yield Revision.new({
214 yield Revision.new({
215 :time => date,
215 :time => date,
216 :author => author,
216 :author => author,
217 :message => commit_log.chomp,
217 :message => commit_log.chomp,
218 :paths => [{
218 :paths => [{
219 :revision => revision,
219 :revision => revision,
220 :branch => revBranch,
220 :branch => revBranch,
221 :path => entry_path,
221 :path => entry_path,
222 :name => entry_name,
222 :name => entry_name,
223 :kind => 'file',
223 :kind => 'file',
224 :action => file_state
224 :action => file_state
225 }]
225 }]
226 })
226 })
227 end
227 end
228 commit_log = String.new
228 commit_log = String.new
229 revision = nil
229 revision = nil
230 if /^#{ENDLOG}/ =~ line
230 if /^#{ENDLOG}/ =~ line
231 state = "entry_start"
231 state = "entry_start"
232 end
232 end
233 next
233 next
234 end
234 end
235
235
236 if /^branches: (.+)$/ =~ line
236 if /^branches: (.+)$/ =~ line
237 # TODO: version.branch = $1
237 # TODO: version.branch = $1
238 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
238 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
239 revision = $1
239 revision = $1
240 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
240 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
241 date = Time.parse($1)
241 date = Time.parse($1)
242 # TODO: This regexp fails in some non UTF-8 chars on Ruby 1.8.
242 line_utf8 = scm_iconv('UTF-8', options[:log_encoding], line)
243 author = /author: ([^;]+)/.match(line)[1]
243 author_utf8 = /author: ([^;]+)/.match(line_utf8)[1]
244 file_state = /state: ([^;]+)/.match(line)[1]
244 author = scm_iconv(options[:log_encoding], 'UTF-8', author_utf8)
245 file_state = /state: ([^;]+)/.match(line)[1]
245 # TODO:
246 # TODO:
246 # linechanges only available in CVS....
247 # linechanges only available in CVS....
247 # maybe a feature our SVN implementation.
248 # maybe a feature our SVN implementation.
248 # I'm sure, they are useful for stats or something else
249 # I'm sure, they are useful for stats or something else
249 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
250 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
250 # unless linechanges.nil?
251 # unless linechanges.nil?
251 # version.line_plus = linechanges[1]
252 # version.line_plus = linechanges[1]
252 # version.line_minus = linechanges[2]
253 # version.line_minus = linechanges[2]
253 # else
254 # else
254 # version.line_plus = 0
255 # version.line_plus = 0
255 # version.line_minus = 0
256 # version.line_minus = 0
256 # end
257 # end
257 else
258 else
258 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
259 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
259 end
260 end
260 end
261 end
261 end
262 end
262 end
263 end
263 rescue ScmCommandAborted
264 rescue ScmCommandAborted
264 Revisions.new
265 Revisions.new
265 end
266 end
266
267
267 def diff(path, identifier_from, identifier_to=nil)
268 def diff(path, identifier_from, identifier_to=nil)
268 logger.debug "<cvs> diff path:'#{path}'" +
269 logger.debug "<cvs> diff path:'#{path}'" +
269 ",identifier_from #{identifier_from}, identifier_to #{identifier_to}"
270 ",identifier_from #{identifier_from}, identifier_to #{identifier_to}"
270 cmd_args = %w|rdiff -u|
271 cmd_args = %w|rdiff -u|
271 cmd_args << "-r#{identifier_to}"
272 cmd_args << "-r#{identifier_to}"
272 cmd_args << "-r#{identifier_from}"
273 cmd_args << "-r#{identifier_from}"
273 cmd_args << path_with_proj(path)
274 cmd_args << path_with_proj(path)
274 diff = []
275 diff = []
275 scm_cmd(*cmd_args) do |io|
276 scm_cmd(*cmd_args) do |io|
276 io.each_line do |line|
277 io.each_line do |line|
277 diff << line
278 diff << line
278 end
279 end
279 end
280 end
280 diff
281 diff
281 rescue ScmCommandAborted
282 rescue ScmCommandAborted
282 nil
283 nil
283 end
284 end
284
285
285 def cat(path, identifier=nil)
286 def cat(path, identifier=nil)
286 identifier = (identifier) ? identifier : "HEAD"
287 identifier = (identifier) ? identifier : "HEAD"
287 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
288 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
288 cmd_args = %w|-q co|
289 cmd_args = %w|-q co|
289 cmd_args << "-D" << time_to_cvstime(identifier) if identifier
290 cmd_args << "-D" << time_to_cvstime(identifier) if identifier
290 cmd_args << "-p" << path_with_proj(path)
291 cmd_args << "-p" << path_with_proj(path)
291 cat = nil
292 cat = nil
292 scm_cmd(*cmd_args) do |io|
293 scm_cmd(*cmd_args) do |io|
293 io.binmode
294 io.binmode
294 cat = io.read
295 cat = io.read
295 end
296 end
296 cat
297 cat
297 rescue ScmCommandAborted
298 rescue ScmCommandAborted
298 nil
299 nil
299 end
300 end
300
301
301 def annotate(path, identifier=nil)
302 def annotate(path, identifier=nil)
302 identifier = (identifier) ? identifier : "HEAD"
303 identifier = (identifier) ? identifier : "HEAD"
303 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
304 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
304 cmd_args = %w|rannotate|
305 cmd_args = %w|rannotate|
305 cmd_args << "-D" << time_to_cvstime(identifier) if identifier
306 cmd_args << "-D" << time_to_cvstime(identifier) if identifier
306 cmd_args << path_with_proj(path)
307 cmd_args << path_with_proj(path)
307 blame = Annotate.new
308 blame = Annotate.new
308 scm_cmd(*cmd_args) do |io|
309 scm_cmd(*cmd_args) do |io|
309 io.each_line do |line|
310 io.each_line do |line|
310 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
311 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
311 blame.add_line(
312 blame.add_line(
312 $3.rstrip,
313 $3.rstrip,
313 Revision.new(
314 Revision.new(
314 :revision => $1,
315 :revision => $1,
315 :identifier => nil,
316 :identifier => nil,
316 :author => $2.strip
317 :author => $2.strip
317 ))
318 ))
318 end
319 end
319 end
320 end
320 blame
321 blame
321 rescue ScmCommandAborted
322 rescue ScmCommandAborted
322 Annotate.new
323 Annotate.new
323 end
324 end
324
325
325 private
326 private
326
327
327 # Returns the root url without the connexion string
328 # Returns the root url without the connexion string
328 # :pserver:anonymous@foo.bar:/path => /path
329 # :pserver:anonymous@foo.bar:/path => /path
329 # :ext:cvsservername:/path => /path
330 # :ext:cvsservername:/path => /path
330 def root_url_path
331 def root_url_path
331 root_url.to_s.gsub(/^:.+:\d*/, '')
332 root_url.to_s.gsub(/^:.+:\d*/, '')
332 end
333 end
333
334
334 # convert a date/time into the CVS-format
335 # convert a date/time into the CVS-format
335 def time_to_cvstime(time)
336 def time_to_cvstime(time)
336 return nil if time.nil?
337 return nil if time.nil?
337 return Time.now if time == 'HEAD'
338 return Time.now if time == 'HEAD'
338
339
339 unless time.kind_of? Time
340 unless time.kind_of? Time
340 time = Time.parse(time)
341 time = Time.parse(time)
341 end
342 end
342 return time.strftime("%Y-%m-%d %H:%M:%S")
343 return time.strftime("%Y-%m-%d %H:%M:%S")
343 end
344 end
344
345
345 def time_to_cvstime_rlog(time)
346 def time_to_cvstime_rlog(time)
346 return nil if time.nil?
347 return nil if time.nil?
347 t1 = time.clone.localtime
348 t1 = time.clone.localtime
348 return t1.strftime("%Y-%m-%d %H:%M:%S")
349 return t1.strftime("%Y-%m-%d %H:%M:%S")
349 end
350 end
350
351
351 def normalize_cvs_path(path)
352 def normalize_cvs_path(path)
352 normalize_path(path.gsub(/Attic\//,''))
353 normalize_path(path.gsub(/Attic\//,''))
353 end
354 end
354
355
355 def normalize_path(path)
356 def normalize_path(path)
356 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
357 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
357 end
358 end
358
359
359 def path_with_proj(path)
360 def path_with_proj(path)
360 "#{url}#{with_leading_slash(path)}"
361 "#{url}#{with_leading_slash(path)}"
361 end
362 end
362 private :path_with_proj
363 private :path_with_proj
363
364
364 class Revision < Redmine::Scm::Adapters::Revision
365 class Revision < Redmine::Scm::Adapters::Revision
365 # Returns the readable identifier
366 # Returns the readable identifier
366 def format_identifier
367 def format_identifier
367 revision.to_s
368 revision.to_s
368 end
369 end
369 end
370 end
370
371
371 def scm_cmd(*args, &block)
372 def scm_cmd(*args, &block)
372 full_args = [CVS_BIN, '-d', root_url]
373 full_args = [CVS_BIN, '-d', root_url]
373 full_args += args
374 full_args += args
374 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
375 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
375 if $? && $?.exitstatus != 0
376 if $? && $?.exitstatus != 0
376 raise ScmCommandAborted, "cvs exited with non-zero status: #{$?.exitstatus}"
377 raise ScmCommandAborted, "cvs exited with non-zero status: #{$?.exitstatus}"
377 end
378 end
378 ret
379 ret
379 end
380 end
380 private :scm_cmd
381 private :scm_cmd
381 end
382 end
382
383
383 class CvsRevisionHelper
384 class CvsRevisionHelper
384 attr_accessor :complete_rev, :revision, :base, :branchid
385 attr_accessor :complete_rev, :revision, :base, :branchid
385
386
386 def initialize(complete_rev)
387 def initialize(complete_rev)
387 @complete_rev = complete_rev
388 @complete_rev = complete_rev
388 parseRevision()
389 parseRevision()
389 end
390 end
390
391
391 def branchPoint
392 def branchPoint
392 return @base
393 return @base
393 end
394 end
394
395
395 def branchVersion
396 def branchVersion
396 if isBranchRevision
397 if isBranchRevision
397 return @base+"."+@branchid
398 return @base+"."+@branchid
398 end
399 end
399 return @base
400 return @base
400 end
401 end
401
402
402 def isBranchRevision
403 def isBranchRevision
403 !@branchid.nil?
404 !@branchid.nil?
404 end
405 end
405
406
406 def prevRev
407 def prevRev
407 unless @revision == 0
408 unless @revision == 0
408 return buildRevision( @revision - 1 )
409 return buildRevision( @revision - 1 )
409 end
410 end
410 return buildRevision( @revision )
411 return buildRevision( @revision )
411 end
412 end
412
413
413 def is_in_branch_with_symbol(branch_symbol)
414 def is_in_branch_with_symbol(branch_symbol)
414 bpieces = branch_symbol.split(".")
415 bpieces = branch_symbol.split(".")
415 branch_start = "#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
416 branch_start = "#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
416 return ( branchVersion == branch_start )
417 return ( branchVersion == branch_start )
417 end
418 end
418
419
419 private
420 private
420 def buildRevision(rev)
421 def buildRevision(rev)
421 if rev == 0
422 if rev == 0
422 if @branchid.nil?
423 if @branchid.nil?
423 @base + ".0"
424 @base + ".0"
424 else
425 else
425 @base
426 @base
426 end
427 end
427 elsif @branchid.nil?
428 elsif @branchid.nil?
428 @base + "." + rev.to_s
429 @base + "." + rev.to_s
429 else
430 else
430 @base + "." + @branchid + "." + rev.to_s
431 @base + "." + @branchid + "." + rev.to_s
431 end
432 end
432 end
433 end
433
434
434 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
435 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
435 def parseRevision()
436 def parseRevision()
436 pieces = @complete_rev.split(".")
437 pieces = @complete_rev.split(".")
437 @revision = pieces.last.to_i
438 @revision = pieces.last.to_i
438 baseSize = 1
439 baseSize = 1
439 baseSize += (pieces.size / 2)
440 baseSize += (pieces.size / 2)
440 @base = pieces[0..-baseSize].join(".")
441 @base = pieces[0..-baseSize].join(".")
441 if baseSize > 2
442 if baseSize > 2
442 @branchid = pieces[-2]
443 @branchid = pieces[-2]
443 end
444 end
444 end
445 end
445 end
446 end
446 end
447 end
447 end
448 end
448 end
449 end
@@ -1,67 +1,67
1 require File.expand_path('../../../../../../test_helper', __FILE__)
1 require File.expand_path('../../../../../../test_helper', __FILE__)
2 begin
2 begin
3 require 'mocha'
3 require 'mocha'
4
4
5 class CvsAdapterTest < ActiveSupport::TestCase
5 class CvsAdapterTest < ActiveSupport::TestCase
6
6
7 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
7 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
8 REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
8 REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
9 MODULE_NAME = 'test'
9 MODULE_NAME = 'test'
10
10
11 if File.directory?(REPOSITORY_PATH)
11 if File.directory?(REPOSITORY_PATH)
12 def setup
12 def setup
13 @adapter = Redmine::Scm::Adapters::CvsAdapter.new(MODULE_NAME, REPOSITORY_PATH)
13 @adapter = Redmine::Scm::Adapters::CvsAdapter.new(MODULE_NAME, REPOSITORY_PATH)
14 end
14 end
15
15
16 def test_scm_version
16 def test_scm_version
17 to_test = { "\nConcurrent Versions System (CVS) 1.12.13 (client/server)\n" => [1,12,13],
17 to_test = { "\nConcurrent Versions System (CVS) 1.12.13 (client/server)\n" => [1,12,13],
18 "\r\n1.12.12\r\n1.12.11" => [1,12,12],
18 "\r\n1.12.12\r\n1.12.11" => [1,12,12],
19 "1.12.11\r\n1.12.10\r\n" => [1,12,11]}
19 "1.12.11\r\n1.12.10\r\n" => [1,12,11]}
20 to_test.each do |s, v|
20 to_test.each do |s, v|
21 test_scm_version_for(s, v)
21 test_scm_version_for(s, v)
22 end
22 end
23 end
23 end
24
24
25 def test_revisions_all
25 def test_revisions_all
26 cnt = 0
26 cnt = 0
27 @adapter.revisions('', nil, nil, :with_paths => true) do |revision|
27 @adapter.revisions('', nil, nil, :log_encoding => 'UTF-8') do |revision|
28 cnt += 1
28 cnt += 1
29 end
29 end
30 assert_equal 16, cnt
30 assert_equal 16, cnt
31 end
31 end
32
32
33 def test_revisions_from_rev3
33 def test_revisions_from_rev3
34 rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22)
34 rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22)
35 cnt = 0
35 cnt = 0
36 @adapter.revisions('', rev3_committed_on, nil, :with_paths => true) do |revision|
36 @adapter.revisions('', rev3_committed_on, nil, :log_encoding => 'UTF-8') do |revision|
37 cnt += 1
37 cnt += 1
38 end
38 end
39 assert_equal 4, cnt
39 assert_equal 4, cnt
40 end
40 end
41
41
42 def test_entries_rev3
42 def test_entries_rev3
43 rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22)
43 rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22)
44 entries = @adapter.entries('sources', rev3_committed_on)
44 entries = @adapter.entries('sources', rev3_committed_on)
45 assert_equal 2, entries.size
45 assert_equal 2, entries.size
46 assert_equal entries[0].name, "watchers_controller.rb"
46 assert_equal entries[0].name, "watchers_controller.rb"
47 assert_equal entries[0].lastrev.time, Time.gm(2007, 12, 13, 16, 27, 22)
47 assert_equal entries[0].lastrev.time, Time.gm(2007, 12, 13, 16, 27, 22)
48 end
48 end
49
49
50 private
50 private
51
51
52 def test_scm_version_for(scm_command_version, version)
52 def test_scm_version_for(scm_command_version, version)
53 @adapter.class.expects(:scm_version_from_command_line).returns(scm_command_version)
53 @adapter.class.expects(:scm_version_from_command_line).returns(scm_command_version)
54 assert_equal version, @adapter.class.scm_command_version
54 assert_equal version, @adapter.class.scm_command_version
55 end
55 end
56 else
56 else
57 puts "Cvs test repository NOT FOUND. Skipping unit tests !!!"
57 puts "Cvs test repository NOT FOUND. Skipping unit tests !!!"
58 def test_fake; assert true end
58 def test_fake; assert true end
59 end
59 end
60 end
60 end
61
61
62 rescue LoadError
62 rescue LoadError
63 class CvsMochaFake < ActiveSupport::TestCase
63 class CvsMochaFake < ActiveSupport::TestCase
64 def test_fake; assert(false, "Requires mocha to run those tests") end
64 def test_fake; assert(false, "Requires mocha to run those tests") end
65 end
65 end
66 end
66 end
67
67
General Comments 0
You need to be logged in to leave comments. Login now