Redmine/lib/redmine/scm/adapters/cvs_adapter.rb
Jean-Philippe Lang 438161ad1f Added basic support for CVS and Mercurial SCMs.
Browsing, changesets fetching and diff viewing are implemented.
Only tested with local repositories.

Thanks to Ralph Vater for CVS specific code.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@559 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-12 20:12:05 +00:00

353 lines
13 KiB
Ruby

# 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
CVS_BIN = "cvs"
# 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
def initialize(url, root_url=nil, login=nil, password=nil)
@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
def root_url
@root_url
end
def url
@url
end
def info
logger.debug "<cvs> info"
Info.new({:root_url => @root_url, :lastrev => nil})
end
def get_previous_revision(revision)
CvsRevisionHelper.new(revision).prevRev
end
# Returns the entry identified by path and revision identifier
# or nil if entry doesn't exist in the repository
# this method returns all revisions from one single SCM-Entry
def entry(path=nil, identifier="HEAD")
e = entries(path, identifier)
logger.debug("<cvs-result> #{e.first.inspect}") if e
e ? e.first : nil
end
# 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
cmd = "#{CVS_BIN} -d #{root_url} rls -ed #{path_with_project}"
shellout(cmd) do |io|
io.each_line(){|line|
fields=line.chop.split('/',-1)
logger.debug(">>InspectLine #{fields.inspect}")
if fields[0]!="D"
entries << Entry.new({:name => fields[-5],
#:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
:path => "#{path}/#{fields[-5]}",
:kind => 'file',
:size => nil,
:lastrev => Revision.new({
:revision => fields[-4],
:name => fields[-4],
:time => Time.parse(fields[-3]),
:author => ''
})
})
else
entries << Entry.new({:name => fields[1],
:path => "#{path}/#{fields[1]}",
:kind => 'dir',
:size => nil,
:lastrev => nil
})
end
}
end
return nil if $? && $?.exitstatus != 0
entries.sort_by_name
rescue Errno::ENOENT => e
raise CommandFailed
end
STARTLOG="----------------------------"
ENDLOG ="============================================================================="
# 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}"
path_with_project="#{url}#{with_leading_slash(path)}"
cmd = "#{CVS_BIN} -d #{root_url} rlog"
cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
cmd << " #{path_with_project}"
shellout(cmd) do |io|
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
io.each_line() do |line|
if state!="revision" && /^#{ENDLOG}/ =~ line
commit_log=String.new
revision=nil
state="entry_start"
end
if state=="entry_start"
branch_map=Hash.new
if /^RCS file: #{Regexp.escape(root_url)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
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"
if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
if revision
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}")
yield Revision.new({
:time => date,
:author => author,
:message=>commit_log.chomp,
:paths => [{
:revision => revision,
:branch=> revBranch,
:path=>entry_path,
:name=>entry_name,
:kind=>'file',
:action=>file_state
}]
})
end
commit_log=String.new
revision=nil
if /^#{ENDLOG}/ =~ line
state="entry_start"
end
next
end
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
# version.line_minus = 0
# end
else
commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
end
end
end
end
rescue Errno::ENOENT => e
raise CommandFailed
end
def diff(path, identifier_from, identifier_to=nil, type="inline")
logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
path_with_project="#{url}#{with_leading_slash(path)}"
cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{path_with_project}"
diff = []
shellout(cmd) do |io|
io.each_line do |line|
diff << line
end
end
return nil if $? && $?.exitstatus != 0
DiffTableList.new diff, type
rescue Errno::ENOENT => e
raise CommandFailed
end
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)}"
cmd = "#{CVS_BIN} -d #{root_url} co -r#{identifier} -p #{path_with_project}"
cat = nil
shellout(cmd) do |io|
cat = io.read
end
return nil if $? && $?.exitstatus != 0
cat
rescue Errno::ENOENT => e
raise CommandFailed
end
private
# convert a date/time into the CVS-format
def time_to_cvstime(time)
return nil if time.nil?
unless time.kind_of? Time
time = Time.parse(time)
end
return time.strftime("%Y-%m-%d %H:%M:%S")
end
def normalize_cvs_path(path)
normalize_path(path.gsub(/Attic\//,''))
end
def normalize_path(path)
path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
end
end
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
@base
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