1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 require 'redmine/scm/adapters/abstract_adapter'
23 class CvsAdapter < AbstractAdapter
28 # Guidelines for the input:
29 # url -> the project-path, relative to the cvsroot (eg. module name)
30 # root_url -> the good old, sometimes damned, CVSROOT
31 # login -> unnecessary
32 # password -> unnecessary too
33 def initialize(url, root_url=nil, login=nil, password=nil)
35 @login = login if login && !login.empty?
36 @password = (password || "") if @login
37 #TODO: better Exception here (IllegalArgumentException)
38 raise CommandFailed if root_url.blank?
51 logger.debug "<cvs> info"
52 Info.new({:root_url => @root_url, :lastrev => nil})
55 def get_previous_revision(revision)
56 CvsRevisionHelper.new(revision).prevRev
59 # Returns an Entries collection
60 # or nil if the given path doesn't exist in the repository
61 # this method is used by the repository-browser (aka LIST)
62 def entries(path=nil, identifier=nil)
63 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
64 path_with_project="#{url}#{with_leading_slash(path)}"
66 cmd = "#{CVS_BIN} -d #{root_url} rls -e"
67 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
68 cmd << " #{shell_quote path_with_project}"
71 fields=line.chop.split('/',-1)
72 logger.debug(">>InspectLine #{fields.inspect}")
75 entries << Entry.new({:name => fields[-5],
76 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
77 :path => "#{path}/#{fields[-5]}",
80 :lastrev => Revision.new({
81 :revision => fields[-4],
83 :time => Time.parse(fields[-3]),
88 entries << Entry.new({:name => fields[1],
89 :path => "#{path}/#{fields[1]}",
97 return nil if $? && $?.exitstatus != 0
101 STARTLOG="----------------------------"
102 ENDLOG ="============================================================================="
104 # Returns all revisions found between identifier_from and identifier_to
105 # in the repository. both identifier have to be dates or nil.
106 # these method returns nothing but yield every result in block
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
108 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
110 path_with_project="#{url}#{with_leading_slash(path)}"
111 cmd = "#{CVS_BIN} -d #{root_url} rlog"
112 cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
113 cmd << " #{shell_quote path_with_project}"
114 shellout(cmd) do |io|
117 commit_log=String.new
126 io.each_line() do |line|
128 if state!="revision" && /^#{ENDLOG}/ =~ line
129 commit_log=String.new
134 if state=="entry_start"
136 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
137 entry_path = normalize_cvs_path($1)
138 entry_name = normalize_path(File.basename($1))
139 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
140 elsif /^head: (.+)$/ =~ line
141 entry_headRev = $1 #unless entry.nil?
142 elsif /^symbolic names:/ =~ line
143 state="symbolic" #unless entry.nil?
144 elsif /^#{STARTLOG}/ =~ line
145 commit_log=String.new
149 elsif state=="symbolic"
150 if /^(.*):\s(.*)/ =~ (line.strip)
157 if /^#{STARTLOG}/ =~ line
160 elsif /^#{ENDLOG}/ =~ line
164 elsif state=="revision"
165 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
168 revHelper=CvsRevisionHelper.new(revision)
171 branch_map.each() do |branch_name,branch_point|
172 if revHelper.is_in_branch_with_symbol(branch_point)
173 revBranch=branch_name
177 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
182 :message=>commit_log.chomp,
184 :revision => revision,
194 commit_log=String.new
197 if /^#{ENDLOG}/ =~ line
203 if /^branches: (.+)$/ =~ line
204 #TODO: version.branch = $1
205 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
207 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
208 date = Time.parse($1)
209 author = /author: ([^;]+)/.match(line)[1]
210 file_state = /state: ([^;]+)/.match(line)[1]
211 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
212 # useful for stats or something else
213 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
214 # unless linechanges.nil?
215 # version.line_plus = linechanges[1]
216 # version.line_minus = linechanges[2]
218 # version.line_plus = 0
219 # version.line_minus = 0
222 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
229 def diff(path, identifier_from, identifier_to=nil)
230 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
231 path_with_project="#{url}#{with_leading_slash(path)}"
232 cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
234 shellout(cmd) do |io|
235 io.each_line do |line|
239 return nil if $? && $?.exitstatus != 0
243 def cat(path, identifier=nil)
244 identifier = (identifier) ? identifier : "HEAD"
245 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
246 path_with_project="#{url}#{with_leading_slash(path)}"
247 cmd = "#{CVS_BIN} -d #{root_url} co"
248 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
249 cmd << " -p #{shell_quote path_with_project}"
251 shellout(cmd) do |io|
254 return nil if $? && $?.exitstatus != 0
258 def annotate(path, identifier=nil)
259 identifier = (identifier) ? identifier : "HEAD"
260 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
261 path_with_project="#{url}#{with_leading_slash(path)}"
262 cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
264 shellout(cmd) do |io|
265 io.each_line do |line|
266 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
267 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
270 return nil if $? && $?.exitstatus != 0
276 # Returns the root url without the connexion string
277 # :pserver:anonymous@foo.bar:/path => /path
278 # :ext:cvsservername:/path => /path
280 root_url.to_s.gsub(/^:.+:\d*/, '')
283 # convert a date/time into the CVS-format
284 def time_to_cvstime(time)
285 return nil if time.nil?
286 unless time.kind_of? Time
287 time = Time.parse(time)
289 return time.strftime("%Y-%m-%d %H:%M:%S")
292 def normalize_cvs_path(path)
293 normalize_path(path.gsub(/Attic\//,''))
296 def normalize_path(path)
297 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
301 class CvsRevisionHelper
302 attr_accessor :complete_rev, :revision, :base, :branchid
304 def initialize(complete_rev)
305 @complete_rev = complete_rev
315 return @base+"."+@branchid
326 return buildRevision(@revision-1)
328 return buildRevision(@revision)
331 def is_in_branch_with_symbol(branch_symbol)
332 bpieces=branch_symbol.split(".")
333 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
334 return (branchVersion==branch_start)
338 def buildRevision(rev)
344 @base+"."+@branchid+"."+rev.to_s
348 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
350 pieces=@complete_rev.split(".")
351 @revision=pieces.last.to_i
353 baseSize+=(pieces.size/2)
354 @base=pieces[0..-baseSize].join(".")