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@441: # 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@441: # 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@1136: require_dependency '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@210: CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs" Chris@245: Chris@245: class << self Chris@245: def client_command Chris@245: @@bin ||= CVS_BIN Chris@245: end Chris@245: Chris@245: def sq_bin Chris@909: @@sq_bin ||= shell_quote_command Chris@245: end Chris@245: Chris@245: def client_version Chris@245: @@client_version ||= (scm_command_version || []) Chris@245: end Chris@245: Chris@245: def client_available Chris@245: client_version_above?([1, 12]) Chris@245: end Chris@245: Chris@245: def scm_command_version Chris@245: scm_version = scm_version_from_command_line.dup Chris@245: if scm_version.respond_to?(:force_encoding) Chris@245: scm_version.force_encoding('ASCII-8BIT') Chris@245: end Chris@245: if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m) Chris@245: m[2].scan(%r{\d+}).collect(&:to_i) Chris@245: end Chris@245: end Chris@245: Chris@245: def scm_version_from_command_line Chris@245: shellout("#{sq_bin} --version") { |io| io.read }.to_s Chris@245: end Chris@245: end Chris@245: Chris@0: # Guidelines for the input: Chris@441: # url -> the project-path, relative to the cvsroot (eg. module name) Chris@0: # root_url -> the good old, sometimes damned, CVSROOT Chris@441: # login -> unnecessary Chris@0: # password -> unnecessary too Chris@245: def initialize(url, root_url=nil, login=nil, password=nil, Chris@245: path_encoding=nil) Chris@441: @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding Chris@441: @url = url Chris@441: # TODO: better Exception here (IllegalArgumentException) Chris@441: raise CommandFailed if root_url.blank? Chris@441: @root_url = root_url Chris@441: Chris@441: # These are unused. Chris@441: @login = login if login && !login.empty? Chris@0: @password = (password || "") if @login Chris@0: end Chris@245: Chris@441: def path_encoding Chris@441: @path_encoding Chris@0: end Chris@245: Chris@0: def info Chris@0: logger.debug " info" Chris@0: Info.new({:root_url => @root_url, :lastrev => nil}) Chris@0: end Chris@245: Chris@0: def get_previous_revision(revision) Chris@0: CvsRevisionHelper.new(revision).prevRev Chris@0: end Chris@245: 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@441: def entries(path=nil, identifier=nil, options={}) Chris@0: logger.debug " entries '#{path}' with identifier '#{identifier}'" Chris@441: path_locale = scm_iconv(@path_encoding, 'UTF-8', path) Chris@441: path_locale.force_encoding("ASCII-8BIT") if path_locale.respond_to?(:force_encoding) Chris@0: entries = Entries.new Chris@441: cmd_args = %w|-q rls -e| Chris@441: cmd_args << "-D" << time_to_cvstime_rlog(identifier) if identifier Chris@441: cmd_args << path_with_proj(path) Chris@441: scm_cmd(*cmd_args) do |io| Chris@441: io.each_line() do |line| Chris@441: fields = line.chop.split('/',-1) Chris@0: logger.debug(">>InspectLine #{fields.inspect}") Chris@0: if fields[0]!="D" Chris@441: time = nil Chris@441: # Thu Dec 13 16:27:22 2007 Chris@441: time_l = fields[-3].split(' ') Chris@441: if time_l.size == 5 && time_l[4].length == 4 Chris@441: begin Chris@441: time = Time.parse( Chris@441: "#{time_l[1]} #{time_l[2]} #{time_l[3]} GMT #{time_l[4]}") Chris@441: rescue Chris@441: end Chris@441: end Chris@441: entries << Entry.new( Chris@441: { Chris@441: :name => scm_iconv('UTF-8', @path_encoding, fields[-5]), Chris@0: #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]), Chris@441: :path => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[-5]}"), Chris@0: :kind => 'file', Chris@0: :size => nil, Chris@441: :lastrev => Revision.new( Chris@441: { Chris@441: :revision => fields[-4], Chris@441: :name => scm_iconv('UTF-8', @path_encoding, fields[-4]), Chris@441: :time => time, Chris@441: :author => '' Chris@441: }) Chris@0: }) Chris@0: else Chris@441: entries << Entry.new( Chris@441: { Chris@441: :name => scm_iconv('UTF-8', @path_encoding, fields[1]), Chris@441: :path => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[1]}"), Chris@441: :kind => 'dir', Chris@441: :size => nil, Chris@0: :lastrev => nil Chris@441: }) Chris@0: end Chris@441: end Chris@0: end Chris@0: entries.sort_by_name Chris@441: rescue ScmCommandAborted Chris@441: nil Chris@245: end Chris@0: Chris@0: STARTLOG="----------------------------" Chris@0: ENDLOG ="=============================================================================" Chris@245: 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@441: path_with_project_utf8 = path_with_proj(path) Chris@441: path_with_project_locale = scm_iconv(@path_encoding, 'UTF-8', path_with_project_utf8) Chris@441: logger.debug " revisions path:" + Chris@441: "'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}" Chris@441: cmd_args = %w|-q rlog| Chris@441: cmd_args << "-d" << ">#{time_to_cvstime_rlog(identifier_from)}" if identifier_from Chris@441: cmd_args << path_with_project_utf8 Chris@441: scm_cmd(*cmd_args) do |io| Chris@441: state = "entry_start" Chris@441: commit_log = String.new Chris@441: revision = nil Chris@441: date = nil Chris@441: author = nil Chris@441: entry_path = nil Chris@441: entry_name = nil Chris@441: file_state = nil Chris@441: branch_map = nil Chris@245: io.each_line() do |line| Chris@441: if state != "revision" && /^#{ENDLOG}/ =~ line Chris@441: commit_log = String.new Chris@441: revision = nil Chris@441: state = "entry_start" Chris@0: end Chris@441: if state == "entry_start" Chris@441: branch_map = Hash.new Chris@441: if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project_locale)}(.+),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@441: state = "symbolic" #unless entry.nil? Chris@0: elsif /^#{STARTLOG}/ =~ line Chris@441: commit_log = String.new Chris@441: state = "revision" Chris@441: end Chris@0: next Chris@441: elsif state == "symbolic" Chris@441: if /^(.*):\s(.*)/ =~ (line.strip) Chris@441: branch_map[$1] = $2 Chris@0: else Chris@441: state = "tags" Chris@0: next Chris@441: end Chris@441: elsif state == "tags" Chris@0: if /^#{STARTLOG}/ =~ line Chris@0: commit_log = "" Chris@441: state = "revision" Chris@0: elsif /^#{ENDLOG}/ =~ line Chris@441: state = "head" Chris@0: end Chris@0: next Chris@441: elsif state == "revision" Chris@245: if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line Chris@0: if revision Chris@441: revHelper = CvsRevisionHelper.new(revision) Chris@441: revBranch = "HEAD" Chris@441: branch_map.each() do |branch_name, branch_point| Chris@0: if revHelper.is_in_branch_with_symbol(branch_point) Chris@441: revBranch = branch_name Chris@0: end Chris@0: end Chris@0: logger.debug("********** YIELD Revision #{revision}::#{revBranch}") Chris@245: yield Revision.new({ Chris@441: :time => date, Chris@441: :author => author, Chris@441: :message => commit_log.chomp, Chris@0: :paths => [{ Chris@1115: :revision => revision.dup, Chris@1115: :branch => revBranch.dup, Chris@441: :path => scm_iconv('UTF-8', @path_encoding, entry_path), Chris@441: :name => scm_iconv('UTF-8', @path_encoding, entry_name), Chris@441: :kind => 'file', Chris@441: :action => file_state Chris@441: }] Chris@441: }) Chris@0: end Chris@441: commit_log = String.new Chris@441: revision = nil Chris@0: if /^#{ENDLOG}/ =~ line Chris@441: state = "entry_start" Chris@0: end Chris@0: next Chris@0: end Chris@245: Chris@0: if /^branches: (.+)$/ =~ line Chris@441: # TODO: version.branch = $1 Chris@0: elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line Chris@441: revision = $1 Chris@0: elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line Chris@441: date = Time.parse($1) Chris@441: line_utf8 = scm_iconv('UTF-8', options[:log_encoding], line) Chris@441: author_utf8 = /author: ([^;]+)/.match(line_utf8)[1] Chris@441: author = scm_iconv(options[:log_encoding], 'UTF-8', author_utf8) Chris@441: file_state = /state: ([^;]+)/.match(line)[1] Chris@441: # TODO: Chris@441: # linechanges only available in CVS.... Chris@441: # maybe a feature our SVN implementation. Chris@441: # I'm sure, they are 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@245: # version.line_minus = 0 Chris@245: # end Chris@245: else Chris@0: commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/ Chris@245: end Chris@245: end Chris@0: end Chris@0: end Chris@441: rescue ScmCommandAborted Chris@441: Revisions.new Chris@245: end Chris@245: Chris@0: def diff(path, identifier_from, identifier_to=nil) Chris@441: logger.debug " diff path:'#{path}'" + Chris@441: ",identifier_from #{identifier_from}, identifier_to #{identifier_to}" Chris@441: cmd_args = %w|rdiff -u| Chris@441: cmd_args << "-r#{identifier_to}" Chris@441: cmd_args << "-r#{identifier_from}" Chris@441: cmd_args << path_with_proj(path) Chris@0: diff = [] Chris@441: scm_cmd(*cmd_args) do |io| Chris@0: io.each_line do |line| Chris@0: diff << line Chris@0: end Chris@0: end Chris@0: diff Chris@441: rescue ScmCommandAborted Chris@441: nil Chris@245: end Chris@245: Chris@0: def cat(path, identifier=nil) Chris@0: identifier = (identifier) ? identifier : "HEAD" Chris@0: logger.debug " cat path:'#{path}',identifier #{identifier}" Chris@441: cmd_args = %w|-q co| Chris@441: cmd_args << "-D" << time_to_cvstime(identifier) if identifier Chris@441: cmd_args << "-p" << path_with_proj(path) Chris@0: cat = nil Chris@441: scm_cmd(*cmd_args) do |io| Chris@245: io.binmode Chris@0: cat = io.read Chris@0: end Chris@0: cat Chris@441: rescue ScmCommandAborted Chris@441: nil Chris@245: end Chris@0: Chris@0: def annotate(path, identifier=nil) Chris@441: identifier = (identifier) ? identifier : "HEAD" Chris@0: logger.debug " annotate path:'#{path}',identifier #{identifier}" Chris@441: cmd_args = %w|rannotate| Chris@441: cmd_args << "-D" << time_to_cvstime(identifier) if identifier Chris@441: cmd_args << path_with_proj(path) Chris@0: blame = Annotate.new Chris@441: scm_cmd(*cmd_args) do |io| Chris@0: io.each_line do |line| Chris@0: next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$} Chris@441: blame.add_line( Chris@441: $3.rstrip, Chris@441: Revision.new( Chris@441: :revision => $1, Chris@441: :identifier => nil, Chris@441: :author => $2.strip Chris@441: )) Chris@0: end Chris@0: end Chris@0: blame Chris@441: rescue ScmCommandAborted Chris@441: Annotate.new Chris@0: end Chris@245: Chris@0: private Chris@245: 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@441: time = Time.now if time == 'HEAD' Chris@441: Chris@0: unless time.kind_of? Time Chris@0: time = Time.parse(time) Chris@0: end Chris@441: return time_to_cvstime_rlog(time) Chris@0: end Chris@210: Chris@210: def time_to_cvstime_rlog(time) Chris@210: return nil if time.nil? Chris@210: t1 = time.clone.localtime Chris@210: return t1.strftime("%Y-%m-%d %H:%M:%S") Chris@210: end Chris@441: Chris@0: def normalize_cvs_path(path) Chris@0: normalize_path(path.gsub(/Attic\//,'')) Chris@0: end Chris@441: Chris@0: def normalize_path(path) Chris@0: path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1') Chris@441: end Chris@441: Chris@441: def path_with_proj(path) Chris@441: "#{url}#{with_leading_slash(path)}" Chris@441: end Chris@441: private :path_with_proj Chris@441: Chris@441: class Revision < Redmine::Scm::Adapters::Revision Chris@441: # Returns the readable identifier Chris@441: def format_identifier Chris@441: revision.to_s Chris@441: end Chris@441: end Chris@441: Chris@441: def scm_cmd(*args, &block) Chris@909: full_args = ['-d', root_url] Chris@441: full_args += args Chris@441: full_args_locale = [] Chris@441: full_args.map do |e| Chris@441: full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e) Chris@441: end Chris@909: ret = shellout( Chris@909: self.class.sq_bin + ' ' + full_args_locale.map { |e| shell_quote e.to_s }.join(' '), Chris@909: &block Chris@909: ) Chris@441: if $? && $?.exitstatus != 0 Chris@441: raise ScmCommandAborted, "cvs exited with non-zero status: #{$?.exitstatus}" Chris@441: end Chris@441: ret Chris@441: end Chris@441: private :scm_cmd Chris@441: end Chris@441: Chris@0: class CvsRevisionHelper Chris@0: attr_accessor :complete_rev, :revision, :base, :branchid Chris@441: Chris@0: def initialize(complete_rev) Chris@0: @complete_rev = complete_rev Chris@0: parseRevision() Chris@0: end Chris@441: Chris@0: def branchPoint Chris@0: return @base Chris@0: end Chris@441: Chris@0: def branchVersion Chris@0: if isBranchRevision Chris@0: return @base+"."+@branchid Chris@0: end Chris@0: return @base Chris@0: end Chris@441: Chris@0: def isBranchRevision Chris@0: !@branchid.nil? Chris@0: end Chris@441: Chris@0: def prevRev Chris@441: unless @revision == 0 Chris@441: return buildRevision( @revision - 1 ) Chris@0: end Chris@441: return buildRevision( @revision ) Chris@0: end Chris@441: Chris@0: def is_in_branch_with_symbol(branch_symbol) Chris@441: bpieces = branch_symbol.split(".") Chris@441: branch_start = "#{bpieces[0..-3].join(".")}.#{bpieces[-1]}" Chris@441: return ( branchVersion == branch_start ) Chris@0: end Chris@441: Chris@0: private Chris@0: def buildRevision(rev) Chris@441: if rev == 0 Chris@245: if @branchid.nil? Chris@441: @base + ".0" Chris@245: else Chris@245: @base Chris@245: end Chris@441: elsif @branchid.nil? Chris@441: @base + "." + rev.to_s Chris@0: else Chris@441: @base + "." + @branchid + "." + rev.to_s Chris@0: end Chris@0: end Chris@441: Chris@0: # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15 Chris@0: def parseRevision() Chris@441: pieces = @complete_rev.split(".") Chris@441: @revision = pieces.last.to_i Chris@441: baseSize = 1 Chris@441: baseSize += (pieces.size / 2) Chris@441: @base = pieces[0..-baseSize].join(".") Chris@0: if baseSize > 2 Chris@441: @branchid = pieces[-2] Chris@441: end Chris@0: end Chris@0: end Chris@0: end Chris@0: end Chris@0: end