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