Chris@0: # redMine - project management software Chris@0: # Copyright (C) 2006-2007 Jean-Philippe Lang Chris@0: # Chris@0: # This program is free software; you can redistribute it and/or Chris@0: # modify it under the terms of the GNU General Public License Chris@0: # as published by the Free Software Foundation; either version 2 Chris@0: # of the License, or (at your option) any later version. Chris@0: # Chris@0: # This program is distributed in the hope that it will be useful, Chris@0: # but WITHOUT ANY WARRANTY; without even the implied warranty of Chris@0: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Chris@0: # GNU General Public License for more details. Chris@0: # Chris@0: # You should have received a copy of the GNU General Public License Chris@0: # along with this program; if not, write to the Free Software Chris@0: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Chris@0: Chris@0: require 'redmine/scm/adapters/abstract_adapter' Chris@0: Chris@0: module Redmine Chris@0: module Scm Chris@0: module Adapters Chris@0: class CvsAdapter < AbstractAdapter Chris@0: Chris@0: # CVS executable name Chris@0: CVS_BIN = "cvs" Chris@0: Chris@0: # Guidelines for the input: Chris@0: # url -> the project-path, relative to the cvsroot (eg. module name) Chris@0: # root_url -> the good old, sometimes damned, CVSROOT Chris@0: # login -> unnecessary Chris@0: # password -> unnecessary too Chris@0: def initialize(url, root_url=nil, login=nil, password=nil) Chris@0: @url = url Chris@0: @login = login if login && !login.empty? Chris@0: @password = (password || "") if @login Chris@0: #TODO: better Exception here (IllegalArgumentException) Chris@0: raise CommandFailed if root_url.blank? Chris@0: @root_url = root_url Chris@0: end Chris@0: Chris@0: def root_url Chris@0: @root_url Chris@0: end Chris@0: Chris@0: def url Chris@0: @url Chris@0: end Chris@0: Chris@0: def info Chris@0: logger.debug " info" Chris@0: Info.new({:root_url => @root_url, :lastrev => nil}) Chris@0: end Chris@0: Chris@0: def get_previous_revision(revision) Chris@0: CvsRevisionHelper.new(revision).prevRev Chris@0: end Chris@0: Chris@0: # Returns an Entries collection Chris@0: # or nil if the given path doesn't exist in the repository Chris@0: # this method is used by the repository-browser (aka LIST) Chris@0: def entries(path=nil, identifier=nil) Chris@0: logger.debug " entries '#{path}' with identifier '#{identifier}'" Chris@0: path_with_project="#{url}#{with_leading_slash(path)}" Chris@0: entries = Entries.new Chris@117: cmd = "#{CVS_BIN} -d #{shell_quote root_url} rls -e" Chris@0: cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier Chris@0: cmd << " #{shell_quote path_with_project}" Chris@0: shellout(cmd) do |io| Chris@0: io.each_line(){|line| Chris@0: fields=line.chop.split('/',-1) Chris@0: logger.debug(">>InspectLine #{fields.inspect}") Chris@0: Chris@0: if fields[0]!="D" Chris@0: entries << Entry.new({:name => fields[-5], Chris@0: #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]), Chris@0: :path => "#{path}/#{fields[-5]}", Chris@0: :kind => 'file', Chris@0: :size => nil, Chris@0: :lastrev => Revision.new({ Chris@0: :revision => fields[-4], Chris@0: :name => fields[-4], Chris@0: :time => Time.parse(fields[-3]), Chris@0: :author => '' Chris@0: }) Chris@0: }) Chris@0: else Chris@0: entries << Entry.new({:name => fields[1], Chris@0: :path => "#{path}/#{fields[1]}", Chris@0: :kind => 'dir', Chris@0: :size => nil, Chris@0: :lastrev => nil Chris@0: }) Chris@0: end Chris@0: } Chris@0: end Chris@0: return nil if $? && $?.exitstatus != 0 Chris@0: entries.sort_by_name Chris@0: end Chris@0: Chris@0: STARTLOG="----------------------------" Chris@0: ENDLOG ="=============================================================================" Chris@0: Chris@0: # Returns all revisions found between identifier_from and identifier_to Chris@0: # in the repository. both identifier have to be dates or nil. Chris@0: # these method returns nothing but yield every result in block Chris@0: def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block) Chris@0: logger.debug " revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}" Chris@0: Chris@0: path_with_project="#{url}#{with_leading_slash(path)}" Chris@117: cmd = "#{CVS_BIN} -d #{shell_quote root_url} rlog" Chris@0: cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from Chris@0: cmd << " #{shell_quote path_with_project}" Chris@0: shellout(cmd) do |io| Chris@0: state="entry_start" Chris@0: Chris@0: commit_log=String.new Chris@0: revision=nil Chris@0: date=nil Chris@0: author=nil Chris@0: entry_path=nil Chris@0: entry_name=nil Chris@0: file_state=nil Chris@0: branch_map=nil Chris@0: Chris@0: io.each_line() do |line| Chris@0: Chris@0: if state!="revision" && /^#{ENDLOG}/ =~ line Chris@0: commit_log=String.new Chris@0: revision=nil Chris@0: state="entry_start" Chris@0: end Chris@0: Chris@0: if state=="entry_start" Chris@0: branch_map=Hash.new Chris@0: if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line Chris@0: entry_path = normalize_cvs_path($1) Chris@0: entry_name = normalize_path(File.basename($1)) Chris@0: logger.debug("Path #{entry_path} <=> Name #{entry_name}") Chris@0: elsif /^head: (.+)$/ =~ line Chris@0: entry_headRev = $1 #unless entry.nil? Chris@0: elsif /^symbolic names:/ =~ line Chris@0: state="symbolic" #unless entry.nil? Chris@0: elsif /^#{STARTLOG}/ =~ line Chris@0: commit_log=String.new Chris@0: state="revision" Chris@0: end Chris@0: next Chris@0: elsif state=="symbolic" Chris@0: if /^(.*):\s(.*)/ =~ (line.strip) Chris@0: branch_map[$1]=$2 Chris@0: else Chris@0: state="tags" Chris@0: next Chris@0: end Chris@0: elsif state=="tags" Chris@0: if /^#{STARTLOG}/ =~ line Chris@0: commit_log = "" Chris@0: state="revision" Chris@0: elsif /^#{ENDLOG}/ =~ line Chris@0: state="head" Chris@0: end Chris@0: next Chris@0: elsif state=="revision" Chris@0: if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line Chris@0: if revision Chris@0: Chris@0: revHelper=CvsRevisionHelper.new(revision) Chris@0: revBranch="HEAD" Chris@0: Chris@0: branch_map.each() do |branch_name,branch_point| Chris@0: if revHelper.is_in_branch_with_symbol(branch_point) Chris@0: revBranch=branch_name Chris@0: end Chris@0: end Chris@0: Chris@0: logger.debug("********** YIELD Revision #{revision}::#{revBranch}") Chris@0: Chris@0: yield Revision.new({ Chris@0: :time => date, Chris@0: :author => author, Chris@0: :message=>commit_log.chomp, Chris@0: :paths => [{ Chris@0: :revision => revision, Chris@0: :branch=> revBranch, Chris@0: :path=>entry_path, Chris@0: :name=>entry_name, Chris@0: :kind=>'file', Chris@0: :action=>file_state Chris@0: }] Chris@0: }) Chris@0: end Chris@0: Chris@0: commit_log=String.new Chris@0: revision=nil Chris@0: Chris@0: if /^#{ENDLOG}/ =~ line Chris@0: state="entry_start" Chris@0: end Chris@0: next Chris@0: end Chris@0: Chris@0: if /^branches: (.+)$/ =~ line Chris@0: #TODO: version.branch = $1 Chris@0: elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line Chris@0: revision = $1 Chris@0: elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line Chris@0: date = Time.parse($1) Chris@0: author = /author: ([^;]+)/.match(line)[1] Chris@0: file_state = /state: ([^;]+)/.match(line)[1] Chris@0: #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are Chris@0: # useful for stats or something else Chris@0: # linechanges =/lines: \+(\d+) -(\d+)/.match(line) Chris@0: # unless linechanges.nil? Chris@0: # version.line_plus = linechanges[1] Chris@0: # version.line_minus = linechanges[2] Chris@0: # else Chris@0: # version.line_plus = 0 Chris@0: # version.line_minus = 0 Chris@0: # end Chris@0: else Chris@0: commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/ Chris@0: end Chris@0: end Chris@0: end Chris@0: end Chris@0: end Chris@0: Chris@0: def diff(path, identifier_from, identifier_to=nil) Chris@0: logger.debug " diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}" Chris@0: path_with_project="#{url}#{with_leading_slash(path)}" Chris@117: cmd = "#{CVS_BIN} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}" Chris@0: diff = [] Chris@0: shellout(cmd) do |io| Chris@0: io.each_line do |line| Chris@0: diff << line Chris@0: end Chris@0: end Chris@0: return nil if $? && $?.exitstatus != 0 Chris@0: diff Chris@0: end Chris@0: Chris@0: def cat(path, identifier=nil) Chris@0: identifier = (identifier) ? identifier : "HEAD" Chris@0: logger.debug " cat path:'#{path}',identifier #{identifier}" Chris@0: path_with_project="#{url}#{with_leading_slash(path)}" Chris@117: cmd = "#{CVS_BIN} -d #{shell_quote root_url} co" Chris@0: cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier Chris@0: cmd << " -p #{shell_quote path_with_project}" Chris@0: cat = nil Chris@0: shellout(cmd) do |io| Chris@0: cat = io.read Chris@0: end Chris@0: return nil if $? && $?.exitstatus != 0 Chris@0: cat Chris@0: end Chris@0: Chris@0: def annotate(path, identifier=nil) Chris@117: identifier = (identifier) ? identifier.to_i : "HEAD" Chris@0: logger.debug " annotate path:'#{path}',identifier #{identifier}" Chris@0: path_with_project="#{url}#{with_leading_slash(path)}" Chris@117: cmd = "#{CVS_BIN} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}" Chris@0: blame = Annotate.new Chris@0: shellout(cmd) do |io| Chris@0: io.each_line do |line| Chris@0: next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$} Chris@0: blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip)) Chris@0: end Chris@0: end Chris@0: return nil if $? && $?.exitstatus != 0 Chris@0: blame Chris@0: end Chris@0: Chris@0: private Chris@0: Chris@0: # Returns the root url without the connexion string Chris@0: # :pserver:anonymous@foo.bar:/path => /path Chris@0: # :ext:cvsservername:/path => /path Chris@0: def root_url_path Chris@0: root_url.to_s.gsub(/^:.+:\d*/, '') Chris@0: end Chris@0: Chris@0: # convert a date/time into the CVS-format Chris@0: def time_to_cvstime(time) Chris@0: return nil if time.nil? Chris@0: unless time.kind_of? Time Chris@0: time = Time.parse(time) Chris@0: end Chris@0: return time.strftime("%Y-%m-%d %H:%M:%S") Chris@0: end Chris@0: Chris@0: def normalize_cvs_path(path) Chris@0: normalize_path(path.gsub(/Attic\//,'')) Chris@0: end Chris@0: Chris@0: def normalize_path(path) Chris@0: path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1') Chris@0: end Chris@0: end Chris@0: Chris@0: class CvsRevisionHelper Chris@0: attr_accessor :complete_rev, :revision, :base, :branchid Chris@0: Chris@0: def initialize(complete_rev) Chris@0: @complete_rev = complete_rev Chris@0: parseRevision() Chris@0: end Chris@0: Chris@0: def branchPoint Chris@0: return @base Chris@0: end Chris@0: Chris@0: def branchVersion Chris@0: if isBranchRevision Chris@0: return @base+"."+@branchid Chris@0: end Chris@0: return @base Chris@0: end Chris@0: Chris@0: def isBranchRevision Chris@0: !@branchid.nil? Chris@0: end Chris@0: Chris@0: def prevRev Chris@0: unless @revision==0 Chris@0: return buildRevision(@revision-1) Chris@0: end Chris@0: return buildRevision(@revision) Chris@0: end Chris@0: Chris@0: def is_in_branch_with_symbol(branch_symbol) Chris@0: bpieces=branch_symbol.split(".") Chris@0: branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}" Chris@0: return (branchVersion==branch_start) Chris@0: end Chris@0: Chris@0: private Chris@0: def buildRevision(rev) Chris@0: if rev== 0 Chris@0: @base Chris@0: elsif @branchid.nil? Chris@0: @base+"."+rev.to_s Chris@0: else Chris@0: @base+"."+@branchid+"."+rev.to_s Chris@0: end Chris@0: end Chris@0: Chris@0: # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15 Chris@0: def parseRevision() Chris@0: pieces=@complete_rev.split(".") Chris@0: @revision=pieces.last.to_i Chris@0: baseSize=1 Chris@0: baseSize+=(pieces.size/2) Chris@0: @base=pieces[0..-baseSize].join(".") Chris@0: if baseSize > 2 Chris@0: @branchid=pieces[-2] Chris@0: end Chris@0: end Chris@0: end Chris@0: end Chris@0: end Chris@0: end