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