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