OSDN Git Service

Added basic support for CVS and Mercurial SCMs.
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Tue, 12 Jun 2007 20:12:05 +0000 (20:12 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Tue, 12 Jun 2007 20:12:05 +0000 (20:12 +0000)
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

41 files changed:
app/controllers/projects_controller.rb
app/controllers/repositories_controller.rb
app/helpers/application_helper.rb
app/helpers/repositories_helper.rb
app/models/repository.rb
app/models/repository/cvs.rb [new file with mode: 0644]
app/models/repository/mercurial.rb [new file with mode: 0644]
app/models/repository/subversion.rb [new file with mode: 0644]
app/models/svn_repos.rb [deleted file]
app/views/projects/_form.rhtml
app/views/projects/_repository.rhtml [new file with mode: 0644]
app/views/repositories/_dir_list.rhtml
app/views/repositories/_navigation.rhtml
app/views/repositories/_revisions.rhtml
app/views/repositories/changes.rhtml [new file with mode: 0644]
app/views/repositories/revision.rhtml
app/views/repositories/revisions.rhtml
app/views/repositories/show.rhtml
db/migrate/052_add_changes_revision.rb [new file with mode: 0644]
db/migrate/053_add_changes_branch.rb [new file with mode: 0644]
db/migrate/054_add_changesets_scmid.rb [new file with mode: 0644]
db/migrate/055_add_repositories_type.rb [new file with mode: 0644]
db/migrate/056_add_repositories_changes_permission.rb [new file with mode: 0644]
lang/bg.yml
lang/de.yml
lang/en.yml
lang/es.yml
lang/fr.yml
lang/it.yml
lang/ja.yml
lang/nl.yml
lang/pt-br.yml
lang/pt.yml
lang/sv.yml
lang/zh.yml
lib/redmine.rb
lib/redmine/scm/adapters/abstract_adapter.rb [new file with mode: 0644]
lib/redmine/scm/adapters/cvs_adapter.rb [new file with mode: 0644]
lib/redmine/scm/adapters/mercurial_adapter.rb [new file with mode: 0644]
lib/redmine/scm/adapters/subversion_adapter.rb [new file with mode: 0644]
test/unit/repository_test.rb

index 0dc6cba..ead1a22 100644 (file)
@@ -35,6 +35,8 @@ class ProjectsController < ApplicationController
   helper IssuesHelper
   helper :queries
   include QueriesHelper
+  helper :repositories
+  include RepositoriesHelper
   
   def index
     list
@@ -70,7 +72,7 @@ class ProjectsController < ApplicationController
       @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
       @project.custom_values = @custom_values                  
       if params[:repository_enabled] && params[:repository_enabled] == "1"
-        @project.repository = Repository.new
+        @project.repository = Repository.factory(params[:repository_scm])
         @project.repository.attributes = params[:repository]
       end
       if "1" == params[:wiki_enabled]
@@ -116,8 +118,8 @@ class ProjectsController < ApplicationController
         when "0"
           @project.repository = nil
         when "1"
-          @project.repository ||= Repository.new
-          @project.repository.update_attributes params[:repository]
+          @project.repository ||= Repository.factory(params[:repository_scm])
+          @project.repository.update_attributes params[:repository] if @project.repository
         end
       end
       if params[:wiki_enabled]
index 4252e58..21e1997 100644 (file)
@@ -21,42 +21,42 @@ require 'digest/sha1'
 
 class RepositoriesController < ApplicationController
   layout 'base'
-  before_filter :find_project
-  before_filter :authorize, :except => [:stats, :graph]
+  before_filter :find_project, :except => [:update_form]
+  before_filter :authorize, :except => [:update_form, :stats, :graph]
   before_filter :check_project_privacy, :only => [:stats, :graph]
   
   def show
+    # check if new revisions have been committed in the repository
+    @repository.fetch_changesets if Setting.autofetch_changesets?
     # get entries for the browse frame
-    @entries = @repository.scm.entries('')
+    @entries = @repository.entries('')
     show_error and return unless @entries
-    # check if new revisions have been committed in the repository
-    scm_latestrev = @entries.revisions.latest
-    if Setting.autofetch_changesets? && scm_latestrev && ((@repository.latest_changeset.nil?) || (@repository.latest_changeset.revision < scm_latestrev.identifier.to_i))
-      @repository.fetch_changesets
-      @repository.reload
-    end
-    @changesets = @repository.changesets.find(:all, :limit => 5, :order => "committed_on DESC")
+    # latest changesets
+    @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
   end
   
   def browse
-    @entries = @repository.scm.entries(@path, @rev)
-    show_error and return unless @entries
+    @entries = @repository.entries(@path, @rev)
+    show_error and return unless @entries    
+  end
+  
+  def changes
+    @entry = @repository.scm.entry(@path, @rev)
+    show_error and return unless @entry
+    @changes = Change.find(:all, :include => :changeset, 
+                                 :conditions => ["repository_id = ? AND path = ?", @repository.id, @path.with_leading_slash],
+                                 :order => "committed_on DESC")
   end
   
   def revisions
-    unless @path == ''
-      @entry = @repository.scm.entry(@path, @rev)  
-      show_error and return unless @entry
-    end
-    @repository.changesets_with_path @path do
-      @changeset_count = @repository.changesets.count(:select => "DISTINCT #{Changeset.table_name}.id")
-      @changeset_pages = Paginator.new self, @changeset_count,
-                                                                     25,
-                                                                     params['page']                                                            
-      @changesets = @repository.changesets.find(:all,
-                                               :limit  =>  @changeset_pages.items_per_page,
-                                               :offset =>  @changeset_pages.current.offset)
-    end
+    @changeset_count = @repository.changesets.count
+    @changeset_pages = Paginator.new self, @changeset_count,
+                                                                     25,
+                                                                     params['page']                                                            
+    @changesets = @repository.changesets.find(:all,
+                                               :limit  =>  @changeset_pages.items_per_page,
+                                               :offset =>  @changeset_pages.current.offset)
+
     render :action => "revisions", :layout => false if request.xhr?
   end
   
@@ -81,12 +81,12 @@ class RepositoriesController < ApplicationController
   end
   
   def diff
-    @rev_to = (params[:rev_to] && params[:rev_to].to_i > 0) ? params[:rev_to].to_i : (@rev - 1)
+    @rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
     @diff_type = ('sbs' == params[:type]) ? 'sbs' : 'inline'
     
     @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")    
     unless read_fragment(@cache_key)
-      @diff = @repository.scm.diff(@path, @rev, @rev_to, type)
+      @diff = @repository.diff(@path, @rev, @rev_to, type)
       show_error and return unless @diff
     end
   end
@@ -110,6 +110,11 @@ class RepositoriesController < ApplicationController
     end
   end
   
+  def update_form
+    @repository = Repository.factory(params[:repository_scm])
+    render :partial => 'projects/repository', :locals => {:repository => @repository}
+  end
+  
 private
   def find_project
     @project = Project.find(params[:id])
@@ -117,7 +122,7 @@ private
     render_404 and return false unless @repository
     @path = params[:path].squeeze('/') if params[:path]
     @path ||= ''
-    @rev = params[:rev].to_i if params[:rev] and params[:rev].to_i > 0
+    @rev = params[:rev].to_i if params[:rev]
   rescue ActiveRecord::RecordNotFound
     render_404
   end
@@ -218,3 +223,9 @@ class Date
     (date.year - self.year)*52 + (date.cweek - self.cweek)
   end
 end
+
+class String
+  def with_leading_slash
+    starts_with?('/') ? self : "/#{self}"
+  end
+end
index 12302a0..564a993 100644 (file)
@@ -251,7 +251,9 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
     src = <<-END_SRC
     def #{selector}(field, options = {}) 
       return super if options.delete :no_label
-      label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
+      label_text = l(options[:label]) if options[:label]
+      label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym)
+      label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required)
       label = @template.content_tag("label", label_text, 
                     :class => (@object && @object.errors[field] ? "error" : nil), 
                     :for => (@object_name.to_s + "_" + field.to_s))
index 2c7dcdd..e2058a7 100644 (file)
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 module RepositoriesHelper
+  def repository_field_tags(form, repository)    
+    method = repository.class.name.demodulize.underscore + "_field_tags"
+    send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
+  end
+  
+  def scm_select_tag
+    container = [[]]
+    REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]}
+    select_tag('repository_scm', 
+               options_for_select(container, @project.repository.class.name.demodulize),
+               :disabled => (@project.repository && !@project.repository.new_record?),
+               :onchange => remote_function(:update => "repository_fields", :url => { :controller => 'repositories', :action => 'update_form', :id => @project }, :with => "Form.serialize(this.form)")
+               )
+  end
+  
+  def with_leading_slash(path)
+    path ||= ''
+    path.starts_with?("/") ? "/#{path}" : path
+  end
+
+  def subversion_field_tags(form, repository)
+      content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
+                       '<br />(http://, https://, svn://, file:///)') +
+      content_tag('p', form.text_field(:login, :size => 30)) +
+      content_tag('p', form.password_field(:password, :size => 30))
+  end
+
+  def mercurial_field_tags(form, repository)
+      content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
+  end
+
+  def cvs_field_tags(form, repository)
+      content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
+      content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
+  end
 end
index 692c446..667ef5e 100644 (file)
 
 class Repository < ActiveRecord::Base
   belongs_to :project
-  has_many :changesets, :dependent => :destroy, :order => 'revision DESC'
+  has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.revision DESC"
   has_many :changes, :through => :changesets
-  has_one  :latest_changeset, :class_name => 'Changeset', :foreign_key => :repository_id, :order => 'revision DESC'
-  
-  attr_protected :root_url
-  
-  validates_presence_of :url
-  validates_format_of :url, :with => /^(http|https|svn|file):\/\/.+/i
     
   def scm
-    @scm ||= SvnRepos::Base.new url, root_url, login, password
+    @scm ||= self.scm_adapter.new url, root_url, login, password
     update_attribute(:root_url, @scm.root_url) if root_url.blank?
     @scm
   end
   
-  def url=(str)
-    super if root_url.blank?
+  def scm_name
+    self.class.scm_name
   end
   
-  def changesets_with_path(path="")
-    path = "/#{path}%"
-    path = url.gsub(/^#{root_url}/, '') + path if root_url && root_url != url
-    path.squeeze!("/")
-    # Custom select and joins is done to allow conditions on changes table without loading associated Change objects
-    # Required for changesets with a great number of changes (eg. 100,000)
-    Changeset.with_scope(:find => { :select => "DISTINCT #{Changeset.table_name}.*", :joins => "LEFT OUTER JOIN #{Change.table_name} ON #{Change.table_name}.changeset_id = #{Changeset.table_name}.id", :conditions => ["#{Change.table_name}.path LIKE ?", path] }) do 
-      yield
-    end 
+  def entries(path=nil, identifier=nil)
+    scm.entries(path, identifier)
   end
   
-  def fetch_changesets
-    scm_info = scm.info
-    if scm_info
-      lastrev_identifier = scm_info.lastrev.identifier.to_i
-      if latest_changeset.nil? || latest_changeset.revision < lastrev_identifier
-        logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
-        identifier_from = latest_changeset ? latest_changeset.revision + 1 : 1
-        while (identifier_from <= lastrev_identifier)
-          # loads changesets by batches of 200
-          identifier_to = [identifier_from + 199, lastrev_identifier].min
-          revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
-          transaction do
-            revisions.reverse_each do |revision|
-              changeset = Changeset.create(:repository => self,
-                                           :revision => revision.identifier, 
-                                           :committer => revision.author, 
-                                           :committed_on => revision.time,
-                                           :comments => revision.message)
-              
-              revision.paths.each do |change|
-                Change.create(:changeset => changeset,
-                              :action => change[:action],
-                              :path => change[:path],
-                              :from_path => change[:from_path],
-                              :from_revision => change[:from_revision])
-              end
-            end
-          end unless revisions.nil?
-          identifier_from = identifier_to + 1
-        end
-      end
-    end
+  def diff(path, rev, rev_to, type)
+    scm.diff(path, rev, rev_to, type)
   end
   
+  def latest_changeset
+    @latest_changeset ||= changesets.find(:first)
+  end
+    
   def scan_changesets_for_issue_ids
     self.changesets.each(&:scan_comment_for_issue_ids)
   end
@@ -96,4 +57,19 @@ class Repository < ActiveRecord::Base
   def self.scan_changesets_for_issue_ids
     find(:all).each(&:scan_changesets_for_issue_ids)
   end
+
+  def self.scm_name
+    'Abstract'
+  end
+  
+  def self.available_scm
+    subclasses.collect {|klass| [klass.scm_name, klass.name]}
+  end
+  
+  def self.factory(klass_name, *args)
+    klass = "Repository::#{klass_name}".constantize
+    klass.new(*args)
+  rescue
+    nil
+  end
 end
diff --git a/app/models/repository/cvs.rb b/app/models/repository/cvs.rb
new file mode 100644 (file)
index 0000000..d6d4ed3
--- /dev/null
@@ -0,0 +1,150 @@
+# 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/cvs_adapter'
+require 'digest/sha1'
+
+class Repository::Cvs < Repository
+  validates_presence_of :url, :root_url
+
+  def scm_adapter
+    Redmine::Scm::Adapters::CvsAdapter
+  end
+  
+  def self.scm_name
+    'CVS'
+  end
+  
+  def entry(path, identifier)
+    e = entries(path, identifier)
+    e ? e.first : nil
+  end
+  
+  def entries(path=nil, identifier=nil)
+    entries=scm.entries(path, identifier)
+    if entries
+      entries.each() do |entry|
+        unless entry.lastrev.nil? || entry.lastrev.identifier
+          change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
+          if change
+            entry.lastrev.identifier=change.changeset.revision
+            entry.lastrev.author=change.changeset.committer
+            entry.lastrev.revision=change.revision
+            entry.lastrev.branch=change.branch
+          end
+        end
+      end
+    end
+    entries
+  end
+  
+  def diff(path, rev, rev_to, type)
+    #convert rev to revision. CVS can't handle changesets here
+    diff=[]
+    changeset_from=changesets.find_by_revision(rev)
+    if rev_to.to_i > 0 
+      changeset_to=changesets.find_by_revision(rev_to)
+    end
+    changeset_from.changes.each() do |change_from|
+      
+      revision_from=nil
+      revision_to=nil      
+      
+      revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
+      
+      if revision_from
+        if changeset_to
+          changeset_to.changes.each() do |change_to|
+            revision_to=change_to.revision if change_to.path==change_from.path 
+          end
+        end
+        unless revision_to
+          revision_to=scm.get_previous_revision(revision_from)
+        end
+        diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
+      end
+    end
+    return diff
+  end
+  
+  def fetch_changesets
+    #not the preferred way with CVS. maybe we should introduce always a cron-job for this
+    last_commit = changesets.maximum(:committed_on)
+    
+    # some nifty bits to introduce a commit-id with cvs
+    # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
+    # we now take a guess using the author, the commitlog and the commit-date.
+    
+    # last one is the next step to take. the commit-date is not equal for all 
+    # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
+    # we use a small delta here, to merge all changes belonging to _one_ changeset
+    time_delta=10.seconds
+    
+    transaction do
+      scm.revisions('', last_commit, nil, :with_paths => true) do |revision|
+        # only add the change to the database, if it doen't exists. the cvs log
+        # is not exclusive at all. 
+        unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
+          revision
+          cs=Changeset.find(:first, :conditions=>{
+            :committed_on=>revision.time-time_delta..revision.time+time_delta,
+            :committer=>revision.author,
+            :comments=>revision.message
+          })
+        
+          # create a new changeset.... 
+          unless cs 
+            # we use a negative changeset-number here (just for inserting)
+            # later on, we calculate a continous positive number
+            next_rev = changesets.minimum(:revision)            
+            next_rev = 0 if next_rev.nil? or next_rev > 0 
+            next_rev = next_rev - 1
+            
+            cs=Changeset.create(:repository => self,
+            :revision => next_rev, 
+            :committer => revision.author, 
+            :committed_on => revision.time,
+            :comments => revision.message)
+          end
+        
+          #convert CVS-File-States to internal Action-abbrevations
+          #default action is (M)odified
+          action="M"
+          if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
+            action="A" #add-action always at first revision (= 1.1)
+          elsif revision.paths[0][:action]=="dead"
+            action="D" #dead-state is similar to Delete
+          end
+        
+          Change.create(:changeset => cs,
+          :action => action,
+          :path => scm.with_leading_slash(revision.paths[0][:path]),
+          :revision => revision.paths[0][:revision],
+          :branch => revision.paths[0][:branch]
+          )
+        end
+      end
+      
+      next_rev = [changesets.maximum(:revision) || 0, 0].max
+      changesets.find(:all, :conditions=>["revision < 0"], :order=>"committed_on ASC").each() do |changeset|
+        next_rev = next_rev + 1
+        changeset.revision = next_rev
+        changeset.save!
+      end
+    end
+  end
+end
diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb
new file mode 100644 (file)
index 0000000..5d9ea9c
--- /dev/null
@@ -0,0 +1,81 @@
+# 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/mercurial_adapter'
+
+class Repository::Mercurial < Repository
+  attr_protected :root_url
+  validates_presence_of :url
+
+  def scm_adapter
+    Redmine::Scm::Adapters::MercurialAdapter
+  end
+  
+  def self.scm_name
+    'Mercurial'
+  end
+  
+  def entries(path=nil, identifier=nil)
+    entries=scm.entries(path, identifier)
+    if entries
+      entries.each do |entry|
+        next unless entry.is_file?
+        # Search the DB for the entry's last change
+        change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
+        if change
+          entry.lastrev.identifier = change.changeset.revision
+          entry.lastrev.name = change.changeset.revision
+          entry.lastrev.author = change.changeset.committer
+          entry.lastrev.revision = change.revision
+        end
+      end
+    end
+    entries
+  end
+
+  def fetch_changesets
+    scm_info = scm.info
+    if scm_info
+      # latest revision found in database
+      db_revision = latest_changeset ? latest_changeset.revision : nil
+      # latest revision in the repository
+      scm_revision = scm_info.lastrev.identifier.to_i
+      
+      unless changesets.find_by_revision(scm_revision)
+        revisions = scm.revisions('', db_revision, nil)
+        transaction do
+          revisions.reverse_each do |revision|
+            changeset = Changeset.create(:repository => self,
+                                         :revision => revision.identifier,
+                                         :scmid => revision.scmid,
+                                         :committer => revision.author, 
+                                         :committed_on => revision.time,
+                                         :comments => revision.message)
+            
+            revision.paths.each do |change|
+              Change.create(:changeset => changeset,
+                            :action => change[:action],
+                            :path => change[:path],
+                            :from_path => change[:from_path],
+                            :from_revision => change[:from_revision])
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/app/models/repository/subversion.rb b/app/models/repository/subversion.rb
new file mode 100644 (file)
index 0000000..cc9c975
--- /dev/null
@@ -0,0 +1,69 @@
+# 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/subversion_adapter'
+
+class Repository::Subversion < Repository
+  attr_protected :root_url
+  validates_presence_of :url
+  validates_format_of :url, :with => /^(http|https|svn|file):\/\/.+/i
+
+  def scm_adapter
+    Redmine::Scm::Adapters::SubversionAdapter
+  end
+  
+  def self.scm_name
+    'Subversion'
+  end
+
+  def fetch_changesets
+    scm_info = scm.info
+    if scm_info
+      # latest revision found in database
+      db_revision = latest_changeset ? latest_changeset.revision : 0
+      # latest revision in the repository
+      scm_revision = scm_info.lastrev.identifier.to_i
+      if db_revision < scm_revision
+        logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
+        identifier_from = db_revision + 1
+        while (identifier_from <= scm_revision)
+          # loads changesets by batches of 200
+          identifier_to = [identifier_from + 199, scm_revision].min
+          revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
+          transaction do
+            revisions.reverse_each do |revision|
+              changeset = Changeset.create(:repository => self,
+                                           :revision => revision.identifier, 
+                                           :committer => revision.author, 
+                                           :committed_on => revision.time,
+                                           :comments => revision.message)
+              
+              revision.paths.each do |change|
+                Change.create(:changeset => changeset,
+                              :action => change[:action],
+                              :path => change[:path],
+                              :from_path => change[:from_path],
+                              :from_revision => change[:from_revision])
+              end
+            end
+          end unless revisions.nil?
+          identifier_from = identifier_to + 1
+        end
+      end
+    end
+  end
+end
diff --git a/app/models/svn_repos.rb b/app/models/svn_repos.rb
deleted file mode 100644 (file)
index 45d0b28..0000000
+++ /dev/null
@@ -1,436 +0,0 @@
-# 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 'rexml/document'
-require 'cgi'
-
-module SvnRepos
-
-  class CommandFailed < StandardError #:nodoc:
-  end
-
-  class Base
-        
-    def initialize(url, root_url=nil, login=nil, password=nil)
-      @url = url
-      @login = login if login && !login.empty?
-      @password = (password || "") if @login    
-      @root_url = root_url.blank? ? retrieve_root_url : root_url
-    end
-    
-    def root_url
-      @root_url
-    end
-    
-    def url
-      @url
-    end
-
-    # get info about the svn repository
-    def info
-      cmd = "svn info --xml #{target('')}"
-      cmd << " --username #{@login} --password #{@password}" if @login
-      info = nil
-      shellout(cmd) do |io|
-        begin
-          doc = REXML::Document.new(io)
-          #root_url = doc.elements["info/entry/repository/root"].text          
-          info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text,
-                           :lastrev => Revision.new({
-                             :identifier => doc.elements["info/entry/commit"].attributes['revision'],
-                             :time => Time.parse(doc.elements["info/entry/commit/date"].text),
-                             :author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")
-                           })
-                         })
-        rescue
-        end
-      end
-      return nil if $? && $?.exitstatus != 0
-      info
-    rescue Errno::ENOENT => e
-      return nil
-    end
-    
-    # Returns the entry identified by path and revision identifier
-    # or nil if entry doesn't exist in the repository
-    def entry(path=nil, identifier=nil)
-      e = entries(path, identifier)
-      e ? e.first : nil
-    end
-    
-    # Returns an Entries collection
-    # or nil if the given path doesn't exist in the repository
-    def entries(path=nil, identifier=nil)
-      path ||= ''
-      identifier = 'HEAD' unless identifier and identifier > 0
-      entries = Entries.new
-      cmd = "svn list --xml #{target(path)}@#{identifier}"
-      cmd << " --username #{@login} --password #{@password}" if @login
-      shellout(cmd) do |io|
-        begin
-          doc = REXML::Document.new(io)
-          doc.elements.each("lists/list/entry") do |entry|
-            entries << Entry.new({:name => entry.elements['name'].text,
-                        :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
-                        :kind => entry.attributes['kind'],
-                        :size => (entry.elements['size'] and entry.elements['size'].text).to_i,
-                        :lastrev => Revision.new({
-                          :identifier => entry.elements['commit'].attributes['revision'],
-                          :time => Time.parse(entry.elements['commit'].elements['date'].text),
-                          :author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "")
-                          })
-                        })
-          end
-        rescue
-        end
-      end
-      return nil if $? && $?.exitstatus != 0
-      entries.sort_by_name
-    rescue Errno::ENOENT => e
-      raise CommandFailed
-    end
-
-    def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-      path ||= ''
-      identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0
-      identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
-      revisions = Revisions.new
-      cmd = "svn log --xml -r #{identifier_from}:#{identifier_to}"
-      cmd << " --username #{@login} --password #{@password}" if @login
-      cmd << " --verbose " if  options[:with_paths]
-      cmd << target(path)
-      shellout(cmd) do |io|
-        begin
-          doc = REXML::Document.new(io)
-          doc.elements.each("log/logentry") do |logentry|
-            paths = []
-            logentry.elements.each("paths/path") do |path|
-              paths << {:action => path.attributes['action'],
-                        :path => path.text,
-                        :from_path => path.attributes['copyfrom-path'],
-                        :from_revision => path.attributes['copyfrom-rev']
-                        }
-            end
-            paths.sort! { |x,y| x[:path] <=> y[:path] }
-            
-            revisions << Revision.new({:identifier => logentry.attributes['revision'],
-                          :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
-                          :time => Time.parse(logentry.elements['date'].text),
-                          :message => logentry.elements['msg'].text,
-                          :paths => paths
-                        })
-          end
-        rescue
-        end
-      end
-      return nil if $? && $?.exitstatus != 0
-      revisions
-    rescue Errno::ENOENT => e
-      raise CommandFailed    
-    end
-    
-    def diff(path, identifier_from, identifier_to=nil, type="inline")
-      path ||= ''
-      if identifier_to and identifier_to.to_i > 0
-        identifier_to = identifier_to.to_i 
-      else
-        identifier_to = identifier_from.to_i - 1
-      end
-      cmd = "svn diff -r "
-      cmd << "#{identifier_to}:"
-      cmd << "#{identifier_from}"
-      cmd << "#{target(path)}@#{identifier_from}"
-      cmd << " --username #{@login} --password #{@password}" if @login
-      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 and identifier.to_i > 0) ? identifier.to_i : "HEAD"
-      cmd = "svn cat #{target(path)}@#{identifier}"
-      cmd << " --username #{@login} --password #{@password}" if @login
-      cat = nil
-      shellout(cmd) do |io|
-        io.binmode
-        cat = io.read
-      end
-      return nil if $? && $?.exitstatus != 0
-      cat
-    rescue Errno::ENOENT => e
-      raise CommandFailed    
-    end
-  
-  private    
-    def retrieve_root_url
-      info = self.info
-      info ? info.root_url : nil
-    end
-    
-    def target(path)
-      path ||= ""
-      base = path.match(/^\//) ? root_url : url    
-      " \"" << "#{base}/#{path}".gsub(/["?<>\*]/, '') << "\""
-    end
-    
-    def logger
-      RAILS_DEFAULT_LOGGER
-    end
-    
-    def shellout(cmd, &block)
-      logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
-      IO.popen(cmd, "r+") do |io|
-        io.close_write
-        block.call(io) if block_given?
-      end
-    end
-  end
-  
-  class Entries < Array
-    def sort_by_name
-      sort {|x,y| 
-        if x.kind == y.kind
-          x.name <=> y.name
-        else
-          x.kind <=> y.kind
-        end
-      }   
-    end
-    
-    def revisions
-      revisions ||= Revisions.new(collect{|entry| entry.lastrev})
-    end
-  end
-  
-  class Info
-    attr_accessor :root_url, :lastrev
-    def initialize(attributes={})
-      self.root_url = attributes[:root_url] if attributes[:root_url]
-      self.lastrev = attributes[:lastrev]
-    end
-  end
-  
-  class Entry
-    attr_accessor :name, :path, :kind, :size, :lastrev
-    def initialize(attributes={})
-      self.name = attributes[:name] if attributes[:name]
-      self.path = attributes[:path] if attributes[:path]
-      self.kind = attributes[:kind] if attributes[:kind]
-      self.size = attributes[:size].to_i if attributes[:size]
-      self.lastrev = attributes[:lastrev]
-    end
-    
-    def is_file?
-      'file' == self.kind
-    end
-    
-    def is_dir?
-      'dir' == self.kind
-    end
-    
-    def is_text?
-      Redmine::MimeType.is_type?('text', name)
-    end
-  end
-  
-  class Revisions < Array
-    def latest
-      sort {|x,y| x.time <=> y.time}.last    
-    end 
-  end
-  
-  class Revision
-    attr_accessor :identifier, :author, :time, :message, :paths
-    def initialize(attributes={})
-      self.identifier = attributes[:identifier]
-      self.author = attributes[:author]
-      self.time = attributes[:time]
-      self.message = attributes[:message] || ""
-      self.paths = attributes[:paths]
-    end
-
-  end
-
-  # A line of Diff
-  class Diff
-
-    attr_accessor :nb_line_left
-    attr_accessor :line_left
-    attr_accessor :nb_line_right
-    attr_accessor :line_right
-    attr_accessor :type_diff_right
-    attr_accessor :type_diff_left
-    
-    def initialize ()
-      self.nb_line_left = ''
-      self.nb_line_right = ''
-      self.line_left = ''
-      self.line_right = ''
-      self.type_diff_right = ''
-      self.type_diff_left = ''
-    end
-
-    def inspect
-      puts '### Start Line Diff ###'
-      puts self.nb_line_left
-      puts self.line_left
-      puts self.nb_line_right
-      puts self.line_right
-    end
-  end
-
-  class DiffTableList < Array
-
-    def initialize (diff, type="inline")
-        diff_table = DiffTable.new type
-        diff.each do |line|
-            if line =~ /^Index: (.*)$/
-                self << diff_table if diff_table.length > 1
-                diff_table = DiffTable.new type
-            end
-            a = diff_table.add_line line
-        end
-        self << diff_table
-    end
-  end
-
-  # Class for create a Diff
-  class DiffTable < Hash
-
-    attr_reader :file_name, :line_num_l, :line_num_r    
-
-    # Initialize with a Diff file and the type of Diff View
-    # The type view must be inline or sbs (side_by_side)
-    def initialize (type="inline")
-      @parsing = false
-      @nb_line = 1
-      @start = false
-      @before = 'same'
-      @second = true
-      @type = type
-    end
-
-    # Function for add a line of this Diff
-    def add_line(line)
-      unless @parsing
-        if line =~ /^Index: (.*)$/
-          @file_name = $1
-          return false
-        elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
-          @line_num_l = $2.to_i
-          @line_num_r = $5.to_i
-          @parsing = true
-        end
-      else
-        if line =~ /^_+$/
-          self.delete(self.keys.sort.last)
-          @parsing = false
-          return false
-        elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
-          @line_num_l = $2.to_i
-          @line_num_r = $5.to_i
-        else
-          @nb_line += 1 if parse_line(line, @type)          
-        end
-      end
-      return true
-    end
-
-    def inspect
-      puts '### DIFF TABLE ###'
-      puts "file : #{file_name}"
-      self.each do |d|
-        d.inspect
-      end
-    end
-
-  private
-
-    # Test if is a Side By Side type
-    def sbs?(type, func)
-      if @start and type == "sbs"
-        if @before == func and @second
-          tmp_nb_line = @nb_line
-          self[tmp_nb_line] = Diff.new
-        else
-            @second = false
-            tmp_nb_line = @start
-            @start += 1
-            @nb_line -= 1
-        end
-      else
-        tmp_nb_line = @nb_line
-        @start = @nb_line
-        self[tmp_nb_line] = Diff.new
-        @second = true
-      end
-      unless self[tmp_nb_line]
-        @nb_line += 1
-        self[tmp_nb_line] = Diff.new
-      else
-        self[tmp_nb_line]
-      end
-    end
-
-    # Escape the HTML for the diff
-    def escapeHTML(line)
-        CGI.escapeHTML(line).gsub(/\s/, '&nbsp;')
-    end
-
-    def parse_line (line, type="inline")
-      if line[0, 1] == "+"
-        diff = sbs? type, 'add'
-        @before = 'add'
-        diff.line_left = escapeHTML line[1..-1]
-        diff.nb_line_left = @line_num_l
-        diff.type_diff_left = 'diff_in'
-        @line_num_l += 1
-        true
-      elsif line[0, 1] == "-"
-        diff = sbs? type, 'remove'
-        @before = 'remove'
-        diff.line_right = escapeHTML line[1..-1]
-        diff.nb_line_right = @line_num_r
-        diff.type_diff_right = 'diff_out'
-        @line_num_r += 1
-        true
-      elsif line[0, 1] =~ /\s/
-        @before = 'same'
-        @start = false
-        diff = Diff.new
-        diff.line_right = escapeHTML line[1..-1]
-        diff.nb_line_right = @line_num_r
-        diff.line_left = escapeHTML line[1..-1]
-        diff.nb_line_left = @line_num_l
-        self[@nb_line] = diff
-        @line_num_l += 1
-        @line_num_r += 1
-        true
-      else
-        false
-      end      
-    end
-  end
-end
\ No newline at end of file
index 5f253d4..9eb9330 100644 (file)
 <!--[eoform:project]-->
 </div>
 
-<div class="box"><h3><%= check_box_tag "repository_enabled", 1, !@project.repository.nil?, :onclick => "Element.toggle('repository');" %> <%= l(:label_repository) %></h3>
-<%= hidden_field_tag "repository_enabled", 0 %>
-<div id="repository">
-<% fields_for :repository, @project.repository, { :builder => TabularFormBuilder, :lang => current_language} do |repository| %>
-<p><%= repository.text_field :url, :size => 60, :required => true, :disabled => (@project.repository && !@project.repository.root_url.blank?) %><br />(http://, https://, svn://, file:///)</p>
-<p><%= repository.text_field :login, :size => 30 %></p>
-<p><%= repository.password_field :password, :size => 30 %></p>
-<% end %>
+<div class="box">
+    <h3><%= check_box_tag "repository_enabled", 1, !@project.repository.nil?, :onclick => "Element.toggle('repository');" %> <%= l(:label_repository) %></h3>
+    <%= hidden_field_tag "repository_enabled", 0 %>    
+    <div id="repository">
+    <p class="tabular"><label>SCM</label><%= scm_select_tag %></p>
+    <div id="repository_fields">
+    <%= render :partial => 'projects/repository', :locals => {:repository => @project.repository} if @project.repository %>
+    </div>
+    </div>
 </div>
 <%= javascript_tag "Element.hide('repository');" if @project.repository.nil? %>
-</div>
 
 <div class="box">
 <h3><%= check_box_tag "wiki_enabled", 1, !@project.wiki.nil?, :onclick => "Element.toggle('wiki');" %> <%= l(:label_wiki) %></h3>
@@ -58,4 +58,4 @@
 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
 <%= javascript_include_tag 'calendar/calendar-setup' %>
 <%= stylesheet_link_tag 'calendar' %>
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/views/projects/_repository.rhtml b/app/views/projects/_repository.rhtml
new file mode 100644 (file)
index 0000000..6f79c61
--- /dev/null
@@ -0,0 +1,3 @@
+<% fields_for :repository, repository, { :builder => TabularFormBuilder, :lang => current_language} do |f| %>
+<%= repository_field_tags(f, repository) %>
+<% end %>
index 0e5b712..5555ee8 100644 (file)
 <% total_size = 0
 @entries.each do |entry| %>
 <tr class="<%= cycle 'odd', 'even' %>">
-<td><%= link_to h(entry.name), { :action => (entry.is_dir? ? 'browse' : 'revisions'), :id => @project, :path => entry.path, :rev => @rev }, :class => ("icon " + (entry.is_dir? ? 'icon-folder' : 'icon-file')) %></td>
-<td align="right"><%= number_to_human_size(entry.size) unless entry.is_dir? %></td>
-<td align="right"><%= link_to entry.lastrev.identifier, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier %></td>
-<td align="center"><%= format_time(entry.lastrev.time) %></td>
-<td align="center"><em><%=h entry.lastrev.author %></em></td>
-<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) %>
+<td><%= link_to h(entry.name), { :action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => entry.path, :rev => @rev }, :class => ("icon " + (entry.is_dir? ? 'icon-folder' : 'icon-file')) %></td>
+<td align="right"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
+<td align="right"><%= link_to(entry.lastrev.name, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
+<td align="center"><%= format_time(entry.lastrev.time) if entry.lastrev %></td>
+<td align="center"><em><%=h(entry.lastrev.author) if entry.lastrev %></em></td>
+<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %>
 <td><%=h truncate(changeset.comments, 100) unless changeset.nil? %></td>
 </tr>
-<% total_size += entry.size
+<% total_size += entry.size if entry.size
 end %>
 </tbody>
 </table>
index efc0b9f..823b4f4 100644 (file)
@@ -5,7 +5,8 @@ if 'file' == kind
     filename = dirs.pop
 end
 link_path = ''
-dirs.each do |dir| 
+dirs.each do |dir|
+    next if dir.blank?
     link_path << '/' unless link_path.empty?
     link_path << "#{dir}" 
     %>
@@ -15,4 +16,4 @@ dirs.each do |dir|
     / <%= link_to h(filename), :action => 'revisions', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %>
 <% end %>
 
-<%= "@ #{revision}" if revision %>
\ No newline at end of file
+<%= "@ #{revision}" if revision %>
index faec166..b2bdb6c 100644 (file)
@@ -9,12 +9,13 @@
 <th><%= l(:field_comments) %></th>
 </tr></thead>
 <tbody>
-<% show_diff = entry && entry.is_file? && changesets.size > 1 %>
+<% show_diff = entry && entry.is_file? && revisions.size > 1 %>
 <% line_num = 1 %>
-<% changesets.each do |changeset| %>
+<% revisions.each do |revision| %>
+<% changeset = revision.is_a?(Change) ? revision.changeset : revision %>
 <tr class="<%= cycle 'odd', 'even' %>">
-<th align="center" style="width:3em;"><%= link_to changeset.revision, :action => 'revision', :id => project, :rev => changeset.revision %></th>
-<td align="center" style="width:1em;"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < changesets.size) %></td>
+<th align="center" style="width:3em;"><%= link_to (revision.revision || changeset.revision), :action => 'revision', :id => project, :rev => changeset.revision %></th>
+<td align="center" style="width:1em;"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
 <td align="center" style="width:1em;"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
 <td align="center" style="width:15%"><%= format_time(changeset.committed_on) %></td>
 <td align="center" style="width:15%"><em><%=h changeset.committer %></em></td>
diff --git a/app/views/repositories/changes.rhtml b/app/views/repositories/changes.rhtml
new file mode 100644 (file)
index 0000000..35ce939
--- /dev/null
@@ -0,0 +1,13 @@
+<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2>
+
+<h3><%=h @entry.name %></h3>
+
+<p>
+<% if @entry.is_text? %>
+<%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
+<% end %>
+<%= link_to l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' } %>
+<%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
+</p>
+
+<%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changes, :entry => @entry }%>
index 5cf5c2e..b484bec 100644 (file)
@@ -7,7 +7,9 @@
 
 <h2><%= l(:label_revision) %> <%= @changeset.revision %></h2>
 
-<p><em><%= @changeset.committer %>, <%= format_time(@changeset.committed_on) %></em></p>
+<p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
+<em><%= @changeset.committer %>, <%= format_time(@changeset.committed_on) %></em></p>
+
 <%= textilizable @changeset.comments %>
 
 <% if @changeset.issues.any? %>
@@ -30,7 +32,7 @@
 <tbody>
 <% @changes.each do |change| %>
 <tr class="<%= cycle 'odd', 'even' %>">
-<td><div class="square action_<%= change.action %>"></div> <%= change.path %></td>
+<td><div class="square action_<%= change.action %>"></div> <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank?  %></td>
 <td align="right">
 <% if change.action == "M" %>
 <%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => change.path, :rev => @changeset.revision %>
index 4a5b376..0c2655d 100644 (file)
@@ -5,25 +5,13 @@
 <% end %>
 </div>
 
-<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2>
+<h2><%= l(:label_revision_plural) %></h2>
 
-<% if @entry && @entry.is_file? %>
-<h3><%=h @entry.name %></h3>
-<p>
-<% if @entry.is_text? %>
-<%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
-<% end %>
-<%= link_to l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' } %>
-(<%= number_to_human_size @entry.size %>)</p>
-<% end %>
-
-<h3><%= l(:label_revision_plural) %></h3>
-
-<%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :changesets => @changesets, :entry => @entry }%>
+<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%>
 
 <p><%= pagination_links_full @changeset_pages %>
 [ <%= @changeset_pages.current.first_item %> - <%= @changeset_pages.current.last_item %> / <%= @changeset_count %> ]</p>
 
 <% content_for :header_tags do %>
 <%= stylesheet_link_tag "scm" %>
-<% end %>
\ No newline at end of file
+<% end %>
index 04a58b4..fcf9544 100644 (file)
@@ -2,17 +2,19 @@
 <%= link_to l(:label_statistics), {:action => 'stats', :id => @project}, :class => 'icon icon-stats' %>
 </div>
 
-<h2><%= l(:label_repository) %></h2>
+<h2><%= l(:label_repository) %> (<%= @repository.scm_name %>)</h2>
 
+<% unless @entries.nil? %>
 <h3><%= l(:label_browse) %></h3>
 <%= render :partial => 'dir_list' %>
+<% end %>
 
 <% unless @changesets.empty? %>
 <h3><%= l(:label_latest_revision_plural) %></h3>
-<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :changesets => @changesets, :entry => nil }%>
+<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%>
 <p><%= link_to l(:label_view_revisions), :action => 'revisions', :id => @project %></p>
 <% end %>
 
 <% content_for :header_tags do %>
 <%= stylesheet_link_tag "scm" %>
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/db/migrate/052_add_changes_revision.rb b/db/migrate/052_add_changes_revision.rb
new file mode 100644 (file)
index 0000000..6f58c1a
--- /dev/null
@@ -0,0 +1,9 @@
+class AddChangesRevision < ActiveRecord::Migration
+  def self.up
+    add_column :changes, :revision, :string
+  end
+
+  def self.down
+    remove_column :changes, :revision
+  end
+end
diff --git a/db/migrate/053_add_changes_branch.rb b/db/migrate/053_add_changes_branch.rb
new file mode 100644 (file)
index 0000000..998ce2b
--- /dev/null
@@ -0,0 +1,9 @@
+class AddChangesBranch < ActiveRecord::Migration
+  def self.up
+    add_column :changes, :branch, :string
+  end
+
+  def self.down
+    remove_column :changes, :branch
+  end
+end
diff --git a/db/migrate/054_add_changesets_scmid.rb b/db/migrate/054_add_changesets_scmid.rb
new file mode 100644 (file)
index 0000000..188fa6e
--- /dev/null
@@ -0,0 +1,9 @@
+class AddChangesetsScmid < ActiveRecord::Migration
+  def self.up
+    add_column :changesets, :scmid, :string
+  end
+
+  def self.down
+    remove_column :changesets, :scmid
+  end
+end
diff --git a/db/migrate/055_add_repositories_type.rb b/db/migrate/055_add_repositories_type.rb
new file mode 100644 (file)
index 0000000..599f70a
--- /dev/null
@@ -0,0 +1,11 @@
+class AddRepositoriesType < ActiveRecord::Migration
+  def self.up
+    add_column :repositories, :type, :string    
+    # Set class name for existing SVN repositories
+    Repository.update_all "type = 'Subversion'"
+  end
+
+  def self.down
+    remove_column :repositories, :type
+  end
+end
diff --git a/db/migrate/056_add_repositories_changes_permission.rb b/db/migrate/056_add_repositories_changes_permission.rb
new file mode 100644 (file)
index 0000000..e935149
--- /dev/null
@@ -0,0 +1,9 @@
+class AddRepositoriesChangesPermission < ActiveRecord::Migration
+  def self.up
+    Permission.create :controller => 'repositories', :action => 'changes', :description => 'label_change_plural', :sort => 1475, :is_public => true, :mail_option => 0, :mail_enabled => 0
+  end
+
+  def self.down
+    Permission.find_by_controller_and_action('repositories', 'changes').destroy
+  end
+end
index 6b96264..71d4759 100644 (file)
@@ -167,8 +167,8 @@ setting_host_name: Хост
 setting_text_formatting: Форматиране на текста
 setting_wiki_compression: Wiki компресиране на историята
 setting_feeds_limit: Лимит на Feeds
-setting_autofetch_changesets: Автоматично обработване на commits в SVN склада
-setting_sys_api_enabled: Разрешаване на WS за управление на SVN склада
+setting_autofetch_changesets: Автоматично обработване на commits в склада
+setting_sys_api_enabled: Разрешаване на WS за управление на склада
 setting_commit_ref_keywords: Отбелязващи ключови думи
 setting_commit_fix_keywords: Приключващи ключови думи
 setting_autologin: Autologin
@@ -318,7 +318,7 @@ label_ago: преди дни
 label_contains: съдържа
 label_not_contains: не съдържа
 label_day_plural: дни
-label_repository: SVN Склад
+label_repository: Склад
 label_browse: Разглеждане
 label_modification: %d промяна
 label_modification_plural: %d промени
index feb3ca8..f2965b3 100644 (file)
@@ -67,7 +67,7 @@ notice_successful_delete: Erfolgreiche Löschung.
 notice_successful_connection: Verbindung erfolgreich.
 notice_file_not_found: Anhang besteht nicht oder ist gelöscht worden.
 notice_locking_conflict: Datum wurde von einem anderen Benutzer geändert.
-notice_scm_error: Eintrag und/oder Revision besteht nicht im SVN.
+notice_scm_error: Eintrag und/oder Revision besteht nicht im Projektarchiv.
 notice_not_authorized: You are not authorized to access this page.
 
 mail_subject_lost_password: Ihr redMine Kennwort
@@ -167,7 +167,7 @@ setting_host_name: Host Name
 setting_text_formatting: Textformatierung
 setting_wiki_compression: Wiki-Historie komprimieren
 setting_feeds_limit: Limit Feed Inhalt
-setting_autofetch_changesets: Autofetch SVN commits
+setting_autofetch_changesets: Autofetch commits
 setting_sys_api_enabled: Enable WS for repository management
 setting_commit_ref_keywords: Referencing keywords
 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 @@ label_ago: vor
 label_contains: enthält
 label_not_contains: enthält nicht
 label_day_plural: Tage
-label_repository: SVN Projektarchiv
+label_repository: Projektarchiv
 label_browse: Codebrowser
 label_modification: %d Änderung
 label_modification_plural: %d Änderungen
index 2fa6dda..1cc1260 100644 (file)
@@ -167,7 +167,7 @@ setting_host_name: Host name
 setting_text_formatting: Text formatting
 setting_wiki_compression: Wiki history compression
 setting_feeds_limit: Feed content limit
-setting_autofetch_changesets: Autofetch SVN commits
+setting_autofetch_changesets: Autofetch commits
 setting_sys_api_enabled: Enable WS for repository management
 setting_commit_ref_keywords: Referencing keywords
 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 @@ label_ago: days ago
 label_contains: contains
 label_not_contains: doesn't contain
 label_day_plural: days
-label_repository: SVN Repository
+label_repository: Repository
 label_browse: Browse
 label_modification: %d change
 label_modification_plural: %d changes
index 4b39c34..484a947 100644 (file)
@@ -167,7 +167,7 @@ setting_host_name: Nombre de anfitrión
 setting_text_formatting: Formato de texto
 setting_wiki_compression: Compresión de la historia de Wiki
 setting_feeds_limit: Feed content limit
-setting_autofetch_changesets: Autofetch SVN commits
+setting_autofetch_changesets: Autofetch commits
 setting_sys_api_enabled: Enable WS for repository management
 setting_commit_ref_keywords: Referencing keywords
 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 @@ label_ago: hace
 label_contains: contiene 
 label_not_contains: no contiene
 label_day_plural: días
-label_repository: Depósito SVN
+label_repository: Depósito
 label_browse: Hojear
 label_modification: %d modificación
 label_modification_plural: %d modificaciones
index 822e39d..d0facf0 100644 (file)
@@ -167,7 +167,7 @@ setting_host_name: Nom d'hôte
 setting_text_formatting: Formatage du texte
 setting_wiki_compression: Compression historique wiki
 setting_feeds_limit: Limite du contenu des flux RSS
-setting_autofetch_changesets: Récupération auto. des commits SVN
+setting_autofetch_changesets: Récupération auto. des commits
 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
 setting_commit_ref_keywords: Mot-clés de référencement
 setting_commit_fix_keywords: Mot-clés de résolution
@@ -318,7 +318,7 @@ label_ago: il y a
 label_contains: contient
 label_not_contains: ne contient pas
 label_day_plural: jours
-label_repository: Dépôt SVN
+label_repository: Dépôt
 label_browse: Parcourir
 label_modification: %d modification
 label_modification_plural: %d modifications
@@ -450,7 +450,7 @@ text_length_between: Longueur comprise entre %d et %d caractères.
 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
 text_unallowed_characters: Caractères non autorisés
 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
-text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires SVN
+text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
 
 default_role_manager: Manager
 default_role_developper: Développeur
index 20ec635..a68c410 100644 (file)
@@ -167,7 +167,7 @@ setting_host_name: Nome host
 setting_text_formatting: Formattazione testo
 setting_wiki_compression: Compressione di storia di Wiki
 setting_feeds_limit: Limite contenuti del feed
-setting_autofetch_changesets: Acquisisci automaticamente le commit SVN
+setting_autofetch_changesets: Acquisisci automaticamente le commit
 setting_sys_api_enabled: Abilita WS per la gestione del repository
 setting_commit_ref_keywords: Referencing keywords
 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 @@ label_ago: giorni fa
 label_contains: contiene
 label_not_contains: non contiene
 label_day_plural: giorni
-label_repository: SVN Repository
+label_repository: Repository
 label_browse: Browse
 label_modification: %d modifica
 label_modification_plural: %d modifiche
index e8f41d9..d8dcc61 100644 (file)
@@ -168,7 +168,7 @@ setting_host_name: ホスト名
 setting_text_formatting: テキストの書式
 setting_wiki_compression: Wiki履歴を圧縮する
 setting_feeds_limit: フィード内容の上限
-setting_autofetch_changesets: SVNコミットを自動取得する
+setting_autofetch_changesets: コミットを自動取得する
 setting_sys_api_enabled: リポジトリ管理用のWeb Serviceを有効化する
 setting_commit_ref_keywords: 参照用キーワード
 setting_commit_fix_keywords: 修正用キーワード
@@ -319,7 +319,7 @@ label_ago: 日前
 label_contains: 含む
 label_not_contains: 含まない
 label_day_plural: 日
-label_repository: SVNリポジトリ
+label_repository: リポジトリ
 label_browse: ブラウズ
 label_modification: %d点の変更
 label_modification_plural: %d点の変更
index 29aca8b..3824925 100644 (file)
@@ -167,7 +167,7 @@ setting_host_name: Host naam
 setting_text_formatting: Tekst formaat
 setting_wiki_compression: Wiki geschiedenis comprimeren
 setting_feeds_limit: Feed inhoud limiet
-setting_autofetch_changesets: Haal SVN commits automatisch op
+setting_autofetch_changesets: Haal commits automatisch op
 setting_sys_api_enabled: Gebruik WS voor repository beheer
 setting_commit_ref_keywords: Referencing keywords
 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 @@ label_ago: dagen geleden
 label_contains: bevat
 label_not_contains: bevat niet
 label_day_plural: dagen
-label_repository: SVN Repository
+label_repository: Repository
 label_browse: Blader
 label_modification: %d wijziging
 label_modification_plural: %d wijzigingen
index ea6ad5d..658d236 100644 (file)
@@ -167,7 +167,7 @@ setting_host_name: Servidor
 setting_text_formatting: Formato do texto\r
 setting_wiki_compression: Compactacao do historio do Wiki\r
 setting_feeds_limit: Limite do Feed\r
-setting_autofetch_changesets: Autofetch SVN commits\r
+setting_autofetch_changesets: Autofetch commits\r
 setting_sys_api_enabled: Ativa WS para gerenciamento do repositorio\r
 setting_commit_ref_keywords: Referencing keywords\r
 setting_commit_fix_keywords: Fixing keywords\r
@@ -318,7 +318,7 @@ label_ago: dias atras
 label_contains: contem\r
 label_not_contains: nao contem\r
 label_day_plural: dias\r
-label_repository: SVN Repository\r
+label_repository: Repository\r
 label_browse: Browse\r
 label_modification: %d change\r
 label_modification_plural: %d changes\r
index 71b5fe0..9d71a4a 100644 (file)
@@ -167,7 +167,7 @@ setting_host_name: Servidor
 setting_text_formatting: Formato do texto
 setting_wiki_compression: Compactação do histórico do Wiki
 setting_feeds_limit: Limite do Feed
-setting_autofetch_changesets: Buscar automaticamente commits do SVN
+setting_autofetch_changesets: Buscar automaticamente commits
 setting_sys_api_enabled: Ativa WS para gerenciamento do repositório
 setting_commit_ref_keywords: Palavras-chave de referôncia
 setting_commit_fix_keywords: Palavras-chave fixas
@@ -318,7 +318,7 @@ label_ago: dias atrás
 label_contains: contém
 label_not_contains: não contém
 label_day_plural: dias
-label_repository: Repositório SVN
+label_repository: Repositório
 label_browse: Procurar
 label_modification: %d mudança
 label_modification_plural: %d mudanças
index dffa867..9d53e1b 100644 (file)
@@ -167,7 +167,7 @@ setting_host_name: Värddatornamn
 setting_text_formatting: Textformattering
 setting_wiki_compression: Wiki historiekomprimering
 setting_feeds_limit: Feed innehållsgräns
-setting_autofetch_changesets: Automatisk hämtning av SVN commits
+setting_autofetch_changesets: Automatisk hämtning av commits
 setting_sys_api_enabled: Aktivera WS för repository management
 setting_commit_ref_keywords: Referencing keywords
 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 @@ label_ago: dagar sedan
 label_contains: innehåller
 label_not_contains: innehåller inte
 label_day_plural: dagar
-label_repository: SVN Repositorie
+label_repository: Repositorie
 label_browse: Bläddra
 label_modification: %d ändring
 label_modification_plural: %d ändringar
index bd40141..e317afa 100644 (file)
@@ -170,7 +170,7 @@ setting_host_name: 主机名称
 setting_text_formatting: 文本格式
 setting_wiki_compression: Wiki history compression
 setting_feeds_limit: Feed content limit
-setting_autofetch_changesets: Autofetch SVN commits
+setting_autofetch_changesets: Autofetch commits
 setting_sys_api_enabled: Enable WS for repository management
 setting_commit_ref_keywords: Referencing keywords
 setting_commit_fix_keywords: Fixing keywords
@@ -321,7 +321,7 @@ label_ago: 之前天数
 label_contains: 包含
 label_not_contains: 不包含
 label_day_plural: 天数
-label_repository: SVN 版本库
+label_repository: 版本库
 label_browse: 浏览
 label_modification: %d 个更新
 label_modification_plural: %d 个更新
index 553ea7f..76edeca 100644 (file)
@@ -1,3 +1,5 @@
 require 'redmine/version'
 require 'redmine/mime_type'
 require 'redmine/acts_as_watchable/init'
+
+REDMINE_SUPPORTED_SCM = %w( Subversion Mercurial Cvs )
diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb
new file mode 100644 (file)
index 0000000..b74fa1b
--- /dev/null
@@ -0,0 +1,341 @@
+# 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 'cgi'
+
+module Redmine
+  module Scm
+    module Adapters    
+      class CommandFailed < StandardError #:nodoc:
+      end
+      
+      class AbstractAdapter #:nodoc:
+        def initialize(url, root_url=nil, login=nil, password=nil)
+          @url = url
+          @login = login if login && !login.empty?
+          @password = (password || "") if @login
+          @root_url = root_url.blank? ? retrieve_root_url : root_url
+        end
+        
+        def adapter_name
+          'Abstract'
+        end
+        
+        def root_url
+          @root_url
+        end
+      
+        def url
+          @url
+        end
+      
+        # get info about the svn repository
+        def info
+          return nil
+        end
+        
+        # Returns the entry identified by path and revision identifier
+        # or nil if entry doesn't exist in the repository
+        def entry(path=nil, identifier=nil)
+          e = entries(path, identifier)
+          e ? e.first : nil
+        end
+        
+        # Returns an Entries collection
+        # or nil if the given path doesn't exist in the repository
+        def entries(path=nil, identifier=nil)
+          return nil
+        end
+    
+        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+          return nil
+        end
+        
+        def diff(path, identifier_from, identifier_to=nil, type="inline")
+          return nil
+        end
+        
+        def cat(path, identifier=nil)
+          return nil
+        end
+
+        def with_leading_slash(path)
+          path ||= ''
+          (path[0,1]!="/") ? "/#{path}" : path
+        end
+              
+      private
+        def retrieve_root_url
+          info = self.info
+          info ? info.root_url : nil
+        end
+        
+        def target(path)
+          path ||= ""
+          base = path.match(/^\//) ? root_url : url    
+          " \"" << "#{base}/#{path}".gsub(/["?<>\*]/, '') << "\""
+        end
+            
+        def logger
+          RAILS_DEFAULT_LOGGER
+        end
+      
+        def shellout(cmd, &block)
+          logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
+          IO.popen(cmd, "r+") do |io|
+            io.close_write
+            block.call(io) if block_given?
+          end
+        end  
+      end
+      
+      class Entries < Array
+        def sort_by_name
+          sort {|x,y| 
+            if x.kind == y.kind
+              x.name <=> y.name
+            else
+              x.kind <=> y.kind
+            end
+          }   
+        end
+        
+        def revisions
+          revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
+        end
+      end
+      
+      class Info
+        attr_accessor :root_url, :lastrev
+        def initialize(attributes={})
+          self.root_url = attributes[:root_url] if attributes[:root_url]
+          self.lastrev = attributes[:lastrev]
+        end
+      end
+      
+      class Entry
+        attr_accessor :name, :path, :kind, :size, :lastrev
+        def initialize(attributes={})
+          self.name = attributes[:name] if attributes[:name]
+          self.path = attributes[:path] if attributes[:path]
+          self.kind = attributes[:kind] if attributes[:kind]
+          self.size = attributes[:size].to_i if attributes[:size]
+          self.lastrev = attributes[:lastrev]
+        end
+        
+        def is_file?
+          'file' == self.kind
+        end
+        
+        def is_dir?
+          'dir' == self.kind
+        end
+        
+        def is_text?
+          Redmine::MimeType.is_type?('text', name)
+        end
+      end
+      
+      class Revisions < Array
+        def latest
+          sort {|x,y|
+            unless x.time.nil? or y.time.nil?
+              x.time <=> y.time
+            else
+              0
+            end
+          }.last
+        end 
+      end
+      
+      class Revision
+        attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
+        def initialize(attributes={})
+          self.identifier = attributes[:identifier]
+          self.scmid = attributes[:scmid]
+          self.name = attributes[:name] || self.identifier
+          self.author = attributes[:author]
+          self.time = attributes[:time]
+          self.message = attributes[:message] || ""
+          self.paths = attributes[:paths]
+          self.revision = attributes[:revision]
+          self.branch = attributes[:branch]
+        end
+    
+      end
+    
+      # A line of Diff
+      class Diff  
+        attr_accessor :nb_line_left
+        attr_accessor :line_left
+        attr_accessor :nb_line_right
+        attr_accessor :line_right
+        attr_accessor :type_diff_right
+        attr_accessor :type_diff_left
+        
+        def initialize ()
+          self.nb_line_left = ''
+          self.nb_line_right = ''
+          self.line_left = ''
+          self.line_right = ''
+          self.type_diff_right = ''
+          self.type_diff_left = ''
+        end
+    
+        def inspect
+          puts '### Start Line Diff ###'
+          puts self.nb_line_left
+          puts self.line_left
+          puts self.nb_line_right
+          puts self.line_right
+        end
+      end
+    
+      class DiffTableList < Array  
+        def initialize (diff, type="inline")
+            diff_table = DiffTable.new type
+            diff.each do |line|
+                if line =~ /^(Index:|diff) (.*)$/
+                    self << diff_table if diff_table.length > 1
+                    diff_table = DiffTable.new type
+                end
+                a = diff_table.add_line line
+            end
+            self << diff_table
+        end
+      end
+    
+      # Class for create a Diff
+      class DiffTable < Hash  
+        attr_reader :file_name, :line_num_l, :line_num_r    
+    
+        # Initialize with a Diff file and the type of Diff View
+        # The type view must be inline or sbs (side_by_side)
+        def initialize (type="inline")
+          @parsing = false
+          @nb_line = 1
+          @start = false
+          @before = 'same'
+          @second = true
+          @type = type
+        end
+    
+        # Function for add a line of this Diff
+        def add_line(line)
+          unless @parsing
+            if line =~ /^(Index:|diff) (.*)$/
+              @file_name = $2
+              return false
+            elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
+              @line_num_l = $5.to_i
+              @line_num_r = $2.to_i
+              @parsing = true
+            end
+          else
+            if line =~ /^[^\+\-\s@\\]/
+              self.delete(self.keys.sort.last)
+              @parsing = false
+              return false
+            elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
+              @line_num_l = $5.to_i
+              @line_num_r = $2.to_i
+            else
+              @nb_line += 1 if parse_line(line, @type)          
+            end
+          end
+          return true
+        end
+    
+        def inspect
+          puts '### DIFF TABLE ###'
+          puts "file : #{file_name}"
+          self.each do |d|
+            d.inspect
+          end
+        end
+    
+      private  
+        # Test if is a Side By Side type
+        def sbs?(type, func)
+          if @start and type == "sbs"
+            if @before == func and @second
+              tmp_nb_line = @nb_line
+              self[tmp_nb_line] = Diff.new
+            else
+                @second = false
+                tmp_nb_line = @start
+                @start += 1
+                @nb_line -= 1
+            end
+          else
+            tmp_nb_line = @nb_line
+            @start = @nb_line
+            self[tmp_nb_line] = Diff.new
+            @second = true
+          end
+          unless self[tmp_nb_line]
+            @nb_line += 1
+            self[tmp_nb_line] = Diff.new
+          else
+            self[tmp_nb_line]
+          end
+        end
+    
+        # Escape the HTML for the diff
+        def escapeHTML(line)
+            CGI.escapeHTML(line).gsub(/\s/, '&nbsp;')
+        end
+    
+        def parse_line (line, type="inline")
+          if line[0, 1] == "+"
+            diff = sbs? type, 'add'
+            @before = 'add'
+            diff.line_left = escapeHTML line[1..-1]
+            diff.nb_line_left = @line_num_l
+            diff.type_diff_left = 'diff_in'
+            @line_num_l += 1
+            true
+          elsif line[0, 1] == "-"
+            diff = sbs? type, 'remove'
+            @before = 'remove'
+            diff.line_right = escapeHTML line[1..-1]
+            diff.nb_line_right = @line_num_r
+            diff.type_diff_right = 'diff_out'
+            @line_num_r += 1
+            true
+          elsif line[0, 1] =~ /\s/
+            @before = 'same'
+            @start = false
+            diff = Diff.new
+            diff.line_right = escapeHTML line[1..-1]
+            diff.nb_line_right = @line_num_r
+            diff.line_left = escapeHTML line[1..-1]
+            diff.nb_line_left = @line_num_l
+            self[@nb_line] = diff
+            @line_num_l += 1
+            @line_num_r += 1
+            true
+          elsif line[0, 1] = "\\"
+            true
+          else
+            false
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/redmine/scm/adapters/cvs_adapter.rb b/lib/redmine/scm/adapters/cvs_adapter.rb
new file mode 100644 (file)
index 0000000..c5f85f1
--- /dev/null
@@ -0,0 +1,352 @@
+# 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
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
new file mode 100644 (file)
index 0000000..54fa8c4
--- /dev/null
@@ -0,0 +1,163 @@
+# 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 MercurialAdapter < AbstractAdapter
+      
+        # Mercurial executable name
+        HG_BIN = "hg"
+        
+        def info
+          cmd = "#{HG_BIN} -R #{target('')} root"
+          root_url = nil
+          shellout(cmd) do |io|
+            root_url = io.gets
+          end
+          return nil if $? && $?.exitstatus != 0
+          info = Info.new({:root_url => root_url.chomp,
+                           :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
+                         })
+          info
+        rescue Errno::ENOENT => e
+          return nil
+        end
+        
+        def entries(path=nil, identifier=nil)
+          path ||= ''
+          entries = Entries.new
+          cmd = "#{HG_BIN} -R #{target('')} --cwd #{target(path)} locate -X */*/*"
+          cmd << " -r #{identifier.to_i}" if identifier
+          cmd << " * */*"
+          shellout(cmd) do |io|
+            io.each_line do |line|
+              e = line.chomp.split('\\')
+              entries << Entry.new({:name => e.first,
+                                    :path => (path.empty? ? e.first : "#{path}/#{e.first}"),
+                                    :kind => (e.size > 1 ? 'dir' : 'file'),
+                                    :lastrev => Revision.new
+                                    }) unless entries.detect{|entry| entry.name == e.first}
+            end
+          end
+          return nil if $? && $?.exitstatus != 0
+          entries.sort_by_name
+        rescue Errno::ENOENT => e
+          raise CommandFailed
+        end
+  
+        def entry(path=nil, identifier=nil)
+          path ||= ''
+          search_path = path.split('/')[0..-2].join('/')
+          entry_name = path.split('/').last
+          e = entries(search_path, identifier)
+          e ? e.detect{|entry| entry.name == entry_name} : nil
+        end
+          
+        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+          revisions = Revisions.new
+          cmd = "#{HG_BIN} -v -R #{target('')} log"
+          cmd << " -r #{identifier_from.to_i}:" if identifier_from
+          cmd << " --limit #{options[:limit].to_i}" if options[:limit]
+          shellout(cmd) do |io|
+            changeset = {}
+            parsing_descr = false
+            line_feeds = 0
+            
+            io.each_line do |line|
+              if line =~ /^(\w+):\s*(.*)$/
+                key = $1
+                value = $2
+                if parsing_descr && line_feeds > 1
+                  parsing_descr = false
+                  revisions << Revision.new({:identifier => changeset[:changeset].split(':').first.to_i,
+                                             :scmid => changeset[:changeset].split(':').last,
+                                             :author => changeset[:user],
+                                             :time => Time.parse(changeset[:date]),
+                                             :message => changeset[:description],
+                                             :paths => changeset[:files].split.collect{|path| {:action => 'X', :path => "/#{path}"}}
+                  })
+                  changeset = {}
+                end
+                if !parsing_descr
+                  changeset.store key.to_sym, value
+                  if $1 == "description"
+                    parsing_descr = true
+                    line_feeds = 0
+                    next
+                  end
+                end
+              end
+              if parsing_descr
+                changeset[:description] << line
+                line_feeds += 1 if line.chomp.empty?
+              end
+            end
+            revisions << Revision.new({:identifier => changeset[:changeset].split(':').first.to_i,
+                                       :scmid => changeset[:changeset].split(':').last,
+                                       :author => changeset[:user],
+                                       :time => Time.parse(changeset[:date]),
+                                       :message => changeset[:description],
+                                       :paths => changeset[:files].split.collect{|path| {:action => 'X', :path => "/#{path}"}}
+            })
+          end
+          return nil if $? && $?.exitstatus != 0
+          revisions
+        rescue Errno::ENOENT => e
+          raise CommandFailed
+        end
+        
+        def diff(path, identifier_from, identifier_to=nil, type="inline")
+          path ||= ''
+          if identifier_to
+            identifier_to = identifier_to.to_i 
+          else
+            identifier_to = identifier_from.to_i - 1
+          end
+          cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
+          cmd << " -I #{target(path)}" unless path.empty?
+          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)
+          cmd = "#{HG_BIN} -R #{target('')} cat #{target(path)}"
+          cat = nil
+          shellout(cmd) do |io|
+            io.binmode
+            cat = io.read
+          end
+          return nil if $? && $?.exitstatus != 0
+          cat
+        rescue Errno::ENOENT => e
+          raise CommandFailed
+        end
+      end
+    end
+  end
+end
diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb
new file mode 100644 (file)
index 0000000..f58bdb1
--- /dev/null
@@ -0,0 +1,173 @@
+# redMine - project management software\r
+# Copyright (C) 2006-2007  Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\r
+\r
+require 'redmine/scm/adapters/abstract_adapter'\r
+require 'rexml/document'\r
+\r
+module Redmine\r
+  module Scm\r
+    module Adapters    \r
+      class SubversionAdapter < AbstractAdapter\r
+      \r
+        # SVN executable name\r
+        SVN_BIN = "svn"\r
+        \r
+        # Get info about the svn repository\r
+        def info\r
+          cmd = "#{SVN_BIN} info --xml #{target('')}"\r
+          cmd << " --username #{@login} --password #{@password}" if @login\r
+          info = nil\r
+          shellout(cmd) do |io|\r
+            begin\r
+              doc = REXML::Document.new(io)\r
+              #root_url = doc.elements["info/entry/repository/root"].text          \r
+              info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text,\r
+                               :lastrev => Revision.new({\r
+                                 :identifier => doc.elements["info/entry/commit"].attributes['revision'],\r
+                                 :time => Time.parse(doc.elements["info/entry/commit/date"].text),\r
+                                 :author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")\r
+                               })\r
+                             })\r
+            rescue\r
+            end\r
+          end\r
+          return nil if $? && $?.exitstatus != 0\r
+          info\r
+        rescue Errno::ENOENT => e\r
+          return nil\r
+        end\r
+        \r
+        # Returns the entry identified by path and revision identifier\r
+        # or nil if entry doesn't exist in the repository\r
+        def entry(path=nil, identifier=nil)\r
+          e = entries(path, identifier)\r
+          e ? e.first : nil\r
+        end\r
+        \r
+        # Returns an Entries collection\r
+        # or nil if the given path doesn't exist in the repository\r
+        def entries(path=nil, identifier=nil)\r
+          path ||= ''\r
+          identifier = 'HEAD' unless identifier and identifier > 0\r
+          entries = Entries.new\r
+          cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"\r
+          cmd << " --username #{@login} --password #{@password}" if @login\r
+          shellout(cmd) do |io|\r
+            begin\r
+              doc = REXML::Document.new(io)\r
+              doc.elements.each("lists/list/entry") do |entry|\r
+                entries << Entry.new({:name => entry.elements['name'].text,\r
+                            :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),\r
+                            :kind => entry.attributes['kind'],\r
+                            :size => (entry.elements['size'] and entry.elements['size'].text).to_i,\r
+                            :lastrev => Revision.new({\r
+                              :identifier => entry.elements['commit'].attributes['revision'],\r
+                              :time => Time.parse(entry.elements['commit'].elements['date'].text),\r
+                              :author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "")\r
+                              })\r
+                            })\r
+              end\r
+            rescue\r
+            end\r
+          end\r
+          return nil if $? && $?.exitstatus != 0\r
+          entries.sort_by_name\r
+        rescue Errno::ENOENT => e\r
+          raise CommandFailed\r
+        end\r
+    \r
+        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})\r
+          path ||= ''\r
+          identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0\r
+          identifier_to = 1 unless identifier_to and identifier_to.to_i > 0\r
+          revisions = Revisions.new\r
+          cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"\r
+          cmd << " --username #{@login} --password #{@password}" if @login\r
+          cmd << " --verbose " if  options[:with_paths]\r
+          cmd << target(path)\r
+          shellout(cmd) do |io|\r
+            begin\r
+              doc = REXML::Document.new(io)\r
+              doc.elements.each("log/logentry") do |logentry|\r
+                paths = []\r
+                logentry.elements.each("paths/path") do |path|\r
+                  paths << {:action => path.attributes['action'],\r
+                            :path => path.text,\r
+                            :from_path => path.attributes['copyfrom-path'],\r
+                            :from_revision => path.attributes['copyfrom-rev']\r
+                            }\r
+                end\r
+                paths.sort! { |x,y| x[:path] <=> y[:path] }\r
+                \r
+                revisions << Revision.new({:identifier => logentry.attributes['revision'],\r
+                              :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),\r
+                              :time => Time.parse(logentry.elements['date'].text),\r
+                              :message => logentry.elements['msg'].text,\r
+                              :paths => paths\r
+                            })\r
+              end\r
+            rescue\r
+            end\r
+          end\r
+          return nil if $? && $?.exitstatus != 0\r
+          revisions\r
+        rescue Errno::ENOENT => e\r
+          raise CommandFailed    \r
+        end\r
+        \r
+        def diff(path, identifier_from, identifier_to=nil, type="inline")\r
+          path ||= ''\r
+          if identifier_to and identifier_to.to_i > 0\r
+            identifier_to = identifier_to.to_i \r
+          else\r
+            identifier_to = identifier_from.to_i - 1\r
+          end\r
+          cmd = "#{SVN_BIN} diff -r "\r
+          cmd << "#{identifier_to}:"\r
+          cmd << "#{identifier_from}"\r
+          cmd << "#{target(path)}@#{identifier_from}"\r
+          cmd << " --username #{@login} --password #{@password}" if @login\r
+          diff = []\r
+          shellout(cmd) do |io|\r
+            io.each_line do |line|\r
+              diff << line\r
+            end\r
+          end\r
+          return nil if $? && $?.exitstatus != 0\r
+          DiffTableList.new diff, type    \r
+        rescue Errno::ENOENT => e\r
+          raise CommandFailed    \r
+        end\r
+        \r
+        def cat(path, identifier=nil)\r
+          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"\r
+          cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"\r
+          cmd << " --username #{@login} --password #{@password}" if @login\r
+          cat = nil\r
+          shellout(cmd) do |io|\r
+            io.binmode\r
+            cat = io.read\r
+          end\r
+          return nil if $? && $?.exitstatus != 0\r
+          cat\r
+        rescue Errno::ENOENT => e\r
+          raise CommandFailed    \r
+        end\r
+      end\r
+    end\r
+  end\r
+end\r
index fd4fb67..e420b64 100644 (file)
@@ -25,7 +25,7 @@ class RepositoryTest < Test::Unit::TestCase
   end
   \r
   def test_create\r
-    repository = Repository.new(:project => Project.find(2))
+    repository = Repository::Subversion.new(:project => Project.find(2))
     assert !repository.save
   
     repository.url = "svn://localhost"\r
@@ -34,12 +34,6 @@ class RepositoryTest < Test::Unit::TestCase
     
     project = Project.find(2)
     assert_equal repository, project.repository\r
-  end\r
-
-  def test_cant_change_url
-    url = @repository.url
-    @repository.url = "svn://anotherhost"
-    assert_equal  url, @repository.url
   end
   
   def test_scan_changesets_for_issue_ids
@@ -59,12 +53,4 @@ class RepositoryTest < Test::Unit::TestCase
     # ignoring commits referencing an issue of another project
     assert_equal [], Issue.find(4).changesets
   end
-  
-  def test_changesets_with_path
-    @repository.changesets_with_path '/some/path' do
-      assert_equal 1, @repository.changesets.count(:select => "DISTINCT #{Changeset.table_name}.id")
-      changesets = @repository.changesets.find(:all)
-      assert_equal 1, changesets.size
-    end
-  end
 end