annotate lib/redmine/scm/adapters/cvs_adapter.rb @ 8:0c83d98252d9 yuya

* Add custom repo prefix and proper auth realm, remove auth cache (seems like an unwise feature), pass DB handle around, various other bits of tidying
author Chris Cannam
date Thu, 12 Aug 2010 15:31:37 +0100
parents 513646585e45
children af80e5618e9b 8661b858af72
rev   line source
Chris@0 1 # redMine - project management software
Chris@0 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@0 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@0 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 require 'redmine/scm/adapters/abstract_adapter'
Chris@0 19
Chris@0 20 module Redmine
Chris@0 21 module Scm
Chris@0 22 module Adapters
Chris@0 23 class CvsAdapter < AbstractAdapter
Chris@0 24
Chris@0 25 # CVS executable name
Chris@0 26 CVS_BIN = "cvs"
Chris@0 27
Chris@0 28 # Guidelines for the input:
Chris@0 29 # url -> the project-path, relative to the cvsroot (eg. module name)
Chris@0 30 # root_url -> the good old, sometimes damned, CVSROOT
Chris@0 31 # login -> unnecessary
Chris@0 32 # password -> unnecessary too
Chris@0 33 def initialize(url, root_url=nil, login=nil, password=nil)
Chris@0 34 @url = url
Chris@0 35 @login = login if login && !login.empty?
Chris@0 36 @password = (password || "") if @login
Chris@0 37 #TODO: better Exception here (IllegalArgumentException)
Chris@0 38 raise CommandFailed if root_url.blank?
Chris@0 39 @root_url = root_url
Chris@0 40 end
Chris@0 41
Chris@0 42 def root_url
Chris@0 43 @root_url
Chris@0 44 end
Chris@0 45
Chris@0 46 def url
Chris@0 47 @url
Chris@0 48 end
Chris@0 49
Chris@0 50 def info
Chris@0 51 logger.debug "<cvs> info"
Chris@0 52 Info.new({:root_url => @root_url, :lastrev => nil})
Chris@0 53 end
Chris@0 54
Chris@0 55 def get_previous_revision(revision)
Chris@0 56 CvsRevisionHelper.new(revision).prevRev
Chris@0 57 end
Chris@0 58
Chris@0 59 # Returns an Entries collection
Chris@0 60 # or nil if the given path doesn't exist in the repository
Chris@0 61 # this method is used by the repository-browser (aka LIST)
Chris@0 62 def entries(path=nil, identifier=nil)
Chris@0 63 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
Chris@0 64 path_with_project="#{url}#{with_leading_slash(path)}"
Chris@0 65 entries = Entries.new
Chris@0 66 cmd = "#{CVS_BIN} -d #{root_url} rls -e"
Chris@0 67 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
Chris@0 68 cmd << " #{shell_quote path_with_project}"
Chris@0 69 shellout(cmd) do |io|
Chris@0 70 io.each_line(){|line|
Chris@0 71 fields=line.chop.split('/',-1)
Chris@0 72 logger.debug(">>InspectLine #{fields.inspect}")
Chris@0 73
Chris@0 74 if fields[0]!="D"
Chris@0 75 entries << Entry.new({:name => fields[-5],
Chris@0 76 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
Chris@0 77 :path => "#{path}/#{fields[-5]}",
Chris@0 78 :kind => 'file',
Chris@0 79 :size => nil,
Chris@0 80 :lastrev => Revision.new({
Chris@0 81 :revision => fields[-4],
Chris@0 82 :name => fields[-4],
Chris@0 83 :time => Time.parse(fields[-3]),
Chris@0 84 :author => ''
Chris@0 85 })
Chris@0 86 })
Chris@0 87 else
Chris@0 88 entries << Entry.new({:name => fields[1],
Chris@0 89 :path => "#{path}/#{fields[1]}",
Chris@0 90 :kind => 'dir',
Chris@0 91 :size => nil,
Chris@0 92 :lastrev => nil
Chris@0 93 })
Chris@0 94 end
Chris@0 95 }
Chris@0 96 end
Chris@0 97 return nil if $? && $?.exitstatus != 0
Chris@0 98 entries.sort_by_name
Chris@0 99 end
Chris@0 100
Chris@0 101 STARTLOG="----------------------------"
Chris@0 102 ENDLOG ="============================================================================="
Chris@0 103
Chris@0 104 # Returns all revisions found between identifier_from and identifier_to
Chris@0 105 # in the repository. both identifier have to be dates or nil.
Chris@0 106 # these method returns nothing but yield every result in block
Chris@0 107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
Chris@0 108 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
Chris@0 109
Chris@0 110 path_with_project="#{url}#{with_leading_slash(path)}"
Chris@0 111 cmd = "#{CVS_BIN} -d #{root_url} rlog"
Chris@0 112 cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
Chris@0 113 cmd << " #{shell_quote path_with_project}"
Chris@0 114 shellout(cmd) do |io|
Chris@0 115 state="entry_start"
Chris@0 116
Chris@0 117 commit_log=String.new
Chris@0 118 revision=nil
Chris@0 119 date=nil
Chris@0 120 author=nil
Chris@0 121 entry_path=nil
Chris@0 122 entry_name=nil
Chris@0 123 file_state=nil
Chris@0 124 branch_map=nil
Chris@0 125
Chris@0 126 io.each_line() do |line|
Chris@0 127
Chris@0 128 if state!="revision" && /^#{ENDLOG}/ =~ line
Chris@0 129 commit_log=String.new
Chris@0 130 revision=nil
Chris@0 131 state="entry_start"
Chris@0 132 end
Chris@0 133
Chris@0 134 if state=="entry_start"
Chris@0 135 branch_map=Hash.new
Chris@0 136 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
Chris@0 137 entry_path = normalize_cvs_path($1)
Chris@0 138 entry_name = normalize_path(File.basename($1))
Chris@0 139 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
Chris@0 140 elsif /^head: (.+)$/ =~ line
Chris@0 141 entry_headRev = $1 #unless entry.nil?
Chris@0 142 elsif /^symbolic names:/ =~ line
Chris@0 143 state="symbolic" #unless entry.nil?
Chris@0 144 elsif /^#{STARTLOG}/ =~ line
Chris@0 145 commit_log=String.new
Chris@0 146 state="revision"
Chris@0 147 end
Chris@0 148 next
Chris@0 149 elsif state=="symbolic"
Chris@0 150 if /^(.*):\s(.*)/ =~ (line.strip)
Chris@0 151 branch_map[$1]=$2
Chris@0 152 else
Chris@0 153 state="tags"
Chris@0 154 next
Chris@0 155 end
Chris@0 156 elsif state=="tags"
Chris@0 157 if /^#{STARTLOG}/ =~ line
Chris@0 158 commit_log = ""
Chris@0 159 state="revision"
Chris@0 160 elsif /^#{ENDLOG}/ =~ line
Chris@0 161 state="head"
Chris@0 162 end
Chris@0 163 next
Chris@0 164 elsif state=="revision"
Chris@0 165 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
Chris@0 166 if revision
Chris@0 167
Chris@0 168 revHelper=CvsRevisionHelper.new(revision)
Chris@0 169 revBranch="HEAD"
Chris@0 170
Chris@0 171 branch_map.each() do |branch_name,branch_point|
Chris@0 172 if revHelper.is_in_branch_with_symbol(branch_point)
Chris@0 173 revBranch=branch_name
Chris@0 174 end
Chris@0 175 end
Chris@0 176
Chris@0 177 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
Chris@0 178
Chris@0 179 yield Revision.new({
Chris@0 180 :time => date,
Chris@0 181 :author => author,
Chris@0 182 :message=>commit_log.chomp,
Chris@0 183 :paths => [{
Chris@0 184 :revision => revision,
Chris@0 185 :branch=> revBranch,
Chris@0 186 :path=>entry_path,
Chris@0 187 :name=>entry_name,
Chris@0 188 :kind=>'file',
Chris@0 189 :action=>file_state
Chris@0 190 }]
Chris@0 191 })
Chris@0 192 end
Chris@0 193
Chris@0 194 commit_log=String.new
Chris@0 195 revision=nil
Chris@0 196
Chris@0 197 if /^#{ENDLOG}/ =~ line
Chris@0 198 state="entry_start"
Chris@0 199 end
Chris@0 200 next
Chris@0 201 end
Chris@0 202
Chris@0 203 if /^branches: (.+)$/ =~ line
Chris@0 204 #TODO: version.branch = $1
Chris@0 205 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
Chris@0 206 revision = $1
Chris@0 207 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
Chris@0 208 date = Time.parse($1)
Chris@0 209 author = /author: ([^;]+)/.match(line)[1]
Chris@0 210 file_state = /state: ([^;]+)/.match(line)[1]
Chris@0 211 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
Chris@0 212 # useful for stats or something else
Chris@0 213 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
Chris@0 214 # unless linechanges.nil?
Chris@0 215 # version.line_plus = linechanges[1]
Chris@0 216 # version.line_minus = linechanges[2]
Chris@0 217 # else
Chris@0 218 # version.line_plus = 0
Chris@0 219 # version.line_minus = 0
Chris@0 220 # end
Chris@0 221 else
Chris@0 222 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
Chris@0 223 end
Chris@0 224 end
Chris@0 225 end
Chris@0 226 end
Chris@0 227 end
Chris@0 228
Chris@0 229 def diff(path, identifier_from, identifier_to=nil)
Chris@0 230 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
Chris@0 231 path_with_project="#{url}#{with_leading_slash(path)}"
Chris@0 232 cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
Chris@0 233 diff = []
Chris@0 234 shellout(cmd) do |io|
Chris@0 235 io.each_line do |line|
Chris@0 236 diff << line
Chris@0 237 end
Chris@0 238 end
Chris@0 239 return nil if $? && $?.exitstatus != 0
Chris@0 240 diff
Chris@0 241 end
Chris@0 242
Chris@0 243 def cat(path, identifier=nil)
Chris@0 244 identifier = (identifier) ? identifier : "HEAD"
Chris@0 245 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
Chris@0 246 path_with_project="#{url}#{with_leading_slash(path)}"
Chris@0 247 cmd = "#{CVS_BIN} -d #{root_url} co"
Chris@0 248 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
Chris@0 249 cmd << " -p #{shell_quote path_with_project}"
Chris@0 250 cat = nil
Chris@0 251 shellout(cmd) do |io|
Chris@0 252 cat = io.read
Chris@0 253 end
Chris@0 254 return nil if $? && $?.exitstatus != 0
Chris@0 255 cat
Chris@0 256 end
Chris@0 257
Chris@0 258 def annotate(path, identifier=nil)
Chris@0 259 identifier = (identifier) ? identifier : "HEAD"
Chris@0 260 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
Chris@0 261 path_with_project="#{url}#{with_leading_slash(path)}"
Chris@0 262 cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
Chris@0 263 blame = Annotate.new
Chris@0 264 shellout(cmd) do |io|
Chris@0 265 io.each_line do |line|
Chris@0 266 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
Chris@0 267 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
Chris@0 268 end
Chris@0 269 end
Chris@0 270 return nil if $? && $?.exitstatus != 0
Chris@0 271 blame
Chris@0 272 end
Chris@0 273
Chris@0 274 private
Chris@0 275
Chris@0 276 # Returns the root url without the connexion string
Chris@0 277 # :pserver:anonymous@foo.bar:/path => /path
Chris@0 278 # :ext:cvsservername:/path => /path
Chris@0 279 def root_url_path
Chris@0 280 root_url.to_s.gsub(/^:.+:\d*/, '')
Chris@0 281 end
Chris@0 282
Chris@0 283 # convert a date/time into the CVS-format
Chris@0 284 def time_to_cvstime(time)
Chris@0 285 return nil if time.nil?
Chris@0 286 unless time.kind_of? Time
Chris@0 287 time = Time.parse(time)
Chris@0 288 end
Chris@0 289 return time.strftime("%Y-%m-%d %H:%M:%S")
Chris@0 290 end
Chris@0 291
Chris@0 292 def normalize_cvs_path(path)
Chris@0 293 normalize_path(path.gsub(/Attic\//,''))
Chris@0 294 end
Chris@0 295
Chris@0 296 def normalize_path(path)
Chris@0 297 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
Chris@0 298 end
Chris@0 299 end
Chris@0 300
Chris@0 301 class CvsRevisionHelper
Chris@0 302 attr_accessor :complete_rev, :revision, :base, :branchid
Chris@0 303
Chris@0 304 def initialize(complete_rev)
Chris@0 305 @complete_rev = complete_rev
Chris@0 306 parseRevision()
Chris@0 307 end
Chris@0 308
Chris@0 309 def branchPoint
Chris@0 310 return @base
Chris@0 311 end
Chris@0 312
Chris@0 313 def branchVersion
Chris@0 314 if isBranchRevision
Chris@0 315 return @base+"."+@branchid
Chris@0 316 end
Chris@0 317 return @base
Chris@0 318 end
Chris@0 319
Chris@0 320 def isBranchRevision
Chris@0 321 !@branchid.nil?
Chris@0 322 end
Chris@0 323
Chris@0 324 def prevRev
Chris@0 325 unless @revision==0
Chris@0 326 return buildRevision(@revision-1)
Chris@0 327 end
Chris@0 328 return buildRevision(@revision)
Chris@0 329 end
Chris@0 330
Chris@0 331 def is_in_branch_with_symbol(branch_symbol)
Chris@0 332 bpieces=branch_symbol.split(".")
Chris@0 333 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
Chris@0 334 return (branchVersion==branch_start)
Chris@0 335 end
Chris@0 336
Chris@0 337 private
Chris@0 338 def buildRevision(rev)
Chris@0 339 if rev== 0
Chris@0 340 @base
Chris@0 341 elsif @branchid.nil?
Chris@0 342 @base+"."+rev.to_s
Chris@0 343 else
Chris@0 344 @base+"."+@branchid+"."+rev.to_s
Chris@0 345 end
Chris@0 346 end
Chris@0 347
Chris@0 348 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
Chris@0 349 def parseRevision()
Chris@0 350 pieces=@complete_rev.split(".")
Chris@0 351 @revision=pieces.last.to_i
Chris@0 352 baseSize=1
Chris@0 353 baseSize+=(pieces.size/2)
Chris@0 354 @base=pieces[0..-baseSize].join(".")
Chris@0 355 if baseSize > 2
Chris@0 356 @branchid=pieces[-2]
Chris@0 357 end
Chris@0 358 end
Chris@0 359 end
Chris@0 360 end
Chris@0 361 end
Chris@0 362 end