summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJean-Philippe Lang <jp_lang@yahoo.fr>2007-06-12 20:12:05 +0000
committerJean-Philippe Lang <jp_lang@yahoo.fr>2007-06-12 20:12:05 +0000
commit438161ad1fd37aadbfa3f5a875540730fd6d70c3 (patch)
treeb396b2379835a6f768023cdc0f7042259f560875
parent4dddb606a6d24c7f95e52f08e689464ab6f8b5b8 (diff)
downloadredmine-438161ad1fd37aadbfa3f5a875540730fd6d70c3.tar.gz
redmine-438161ad1fd37aadbfa3f5a875540730fd6d70c3.zip
Added basic support for CVS and Mercurial SCMs.
Browsing, changesets fetching and diff viewing are implemented. Only tested with local repositories. Thanks to Ralph Vater for CVS specific code. git-svn-id: http://redmine.rubyforge.org/svn/trunk@559 e93f8b46-1217-0410-a6f0-8f06a7374b81
-rw-r--r--app/controllers/projects_controller.rb8
-rw-r--r--app/controllers/repositories_controller.rb67
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/repositories_helper.rb35
-rw-r--r--app/models/repository.rb78
-rw-r--r--app/models/repository/cvs.rb150
-rw-r--r--app/models/repository/mercurial.rb81
-rw-r--r--app/models/repository/subversion.rb69
-rw-r--r--app/models/svn_repos.rb436
-rw-r--r--app/views/projects/_form.rhtml20
-rw-r--r--app/views/projects/_repository.rhtml3
-rw-r--r--app/views/repositories/_dir_list.rhtml14
-rw-r--r--app/views/repositories/_navigation.rhtml5
-rw-r--r--app/views/repositories/_revisions.rhtml9
-rw-r--r--app/views/repositories/changes.rhtml13
-rw-r--r--app/views/repositories/revision.rhtml6
-rw-r--r--app/views/repositories/revisions.rhtml18
-rw-r--r--app/views/repositories/show.rhtml8
-rw-r--r--db/migrate/052_add_changes_revision.rb9
-rw-r--r--db/migrate/053_add_changes_branch.rb9
-rw-r--r--db/migrate/054_add_changesets_scmid.rb9
-rw-r--r--db/migrate/055_add_repositories_type.rb11
-rw-r--r--db/migrate/056_add_repositories_changes_permission.rb9
-rw-r--r--lang/bg.yml6
-rw-r--r--lang/de.yml6
-rw-r--r--lang/en.yml4
-rw-r--r--lang/es.yml4
-rw-r--r--lang/fr.yml6
-rw-r--r--lang/it.yml4
-rw-r--r--lang/ja.yml4
-rw-r--r--lang/nl.yml4
-rw-r--r--lang/pt-br.yml4
-rw-r--r--lang/pt.yml4
-rw-r--r--lang/sv.yml4
-rw-r--r--lang/zh.yml4
-rw-r--r--lib/redmine.rb2
-rw-r--r--lib/redmine/scm/adapters/abstract_adapter.rb341
-rw-r--r--lib/redmine/scm/adapters/cvs_adapter.rb352
-rw-r--r--lib/redmine/scm/adapters/mercurial_adapter.rb163
-rw-r--r--lib/redmine/scm/adapters/subversion_adapter.rb173
-rw-r--r--test/unit/repository_test.rb16
41 files changed, 1568 insertions, 604 deletions
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 0dc6cba26..ead1a224a 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -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]
diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb
index 4252e58be..21e1997ab 100644
--- a/app/controllers/repositories_controller.rb
+++ b/app/controllers/repositories_controller.rb
@@ -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
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 12302a073..564a9938f 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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))
diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb
index 2c7dcdd53..e2058a712 100644
--- a/app/helpers/repositories_helper.rb
+++ b/app/helpers/repositories_helper.rb
@@ -16,4 +16,39 @@
# 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
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 692c446d6..667ef5efc 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -17,70 +17,31 @@
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
index 000000000..d6d4ed317
--- /dev/null
+++ b/app/models/repository/cvs.rb
@@ -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
index 000000000..5d9ea9cd4
--- /dev/null
+++ b/app/models/repository/mercurial.rb
@@ -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
index 000000000..cc9c975a3
--- /dev/null
+++ b/app/models/repository/subversion.rb
@@ -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
index 45d0b289a..000000000
--- a/app/models/svn_repos.rb
+++ /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
diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml
index 5f253d401..9eb933035 100644
--- a/app/views/projects/_form.rhtml
+++ b/app/views/projects/_form.rhtml
@@ -27,17 +27,17 @@
<!--[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
index 000000000..6f79c615f
--- /dev/null
+++ b/app/views/projects/_repository.rhtml
@@ -0,0 +1,3 @@
+<% fields_for :repository, repository, { :builder => TabularFormBuilder, :lang => current_language} do |f| %>
+<%= repository_field_tags(f, repository) %>
+<% end %>
diff --git a/app/views/repositories/_dir_list.rhtml b/app/views/repositories/_dir_list.rhtml
index 0e5b712bf..5555ee87e 100644
--- a/app/views/repositories/_dir_list.rhtml
+++ b/app/views/repositories/_dir_list.rhtml
@@ -11,15 +11,15 @@
<% 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>
diff --git a/app/views/repositories/_navigation.rhtml b/app/views/repositories/_navigation.rhtml
index efc0b9ff9..823b4f44f 100644
--- a/app/views/repositories/_navigation.rhtml
+++ b/app/views/repositories/_navigation.rhtml
@@ -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 %>
diff --git a/app/views/repositories/_revisions.rhtml b/app/views/repositories/_revisions.rhtml
index faec16662..b2bdb6c7f 100644
--- a/app/views/repositories/_revisions.rhtml
+++ b/app/views/repositories/_revisions.rhtml
@@ -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
index 000000000..35ce939fc
--- /dev/null
+++ b/app/views/repositories/changes.rhtml
@@ -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 }%>
diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml
index 5cf5c2e41..b484becce 100644
--- a/app/views/repositories/revision.rhtml
+++ b/app/views/repositories/revision.rhtml
@@ -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 %>
diff --git a/app/views/repositories/revisions.rhtml b/app/views/repositories/revisions.rhtml
index 4a5b3766e..0c2655d5f 100644
--- a/app/views/repositories/revisions.rhtml
+++ b/app/views/repositories/revisions.rhtml
@@ -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 %>
diff --git a/app/views/repositories/show.rhtml b/app/views/repositories/show.rhtml
index 04a58b4c9..fcf954473 100644
--- a/app/views/repositories/show.rhtml
+++ b/app/views/repositories/show.rhtml
@@ -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
index 000000000..6f58c1a70
--- /dev/null
+++ b/db/migrate/052_add_changes_revision.rb
@@ -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
index 000000000..998ce2ba5
--- /dev/null
+++ b/db/migrate/053_add_changes_branch.rb
@@ -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
index 000000000..188fa6ef6
--- /dev/null
+++ b/db/migrate/054_add_changesets_scmid.rb
@@ -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
index 000000000..599f70aac
--- /dev/null
+++ b/db/migrate/055_add_repositories_type.rb
@@ -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
index 000000000..e93514900
--- /dev/null
+++ b/db/migrate/056_add_repositories_changes_permission.rb
@@ -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
diff --git a/lang/bg.yml b/lang/bg.yml
index 6b9626499..71d4759ac 100644
--- a/lang/bg.yml
+++ b/lang/bg.yml
@@ -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 промени
diff --git a/lang/de.yml b/lang/de.yml
index feb3ca833..f2965b35a 100644
--- a/lang/de.yml
+++ b/lang/de.yml
@@ -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
diff --git a/lang/en.yml b/lang/en.yml
index 2fa6ddabb..1cc12605b 100644
--- a/lang/en.yml
+++ b/lang/en.yml
@@ -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
diff --git a/lang/es.yml b/lang/es.yml
index 4b39c345e..484a947e5 100644
--- a/lang/es.yml
+++ b/lang/es.yml
@@ -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
diff --git a/lang/fr.yml b/lang/fr.yml
index 822e39da7..d0facf0d6 100644
--- a/lang/fr.yml
+++ b/lang/fr.yml
@@ -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
diff --git a/lang/it.yml b/lang/it.yml
index 20ec63546..a68c4102f 100644
--- a/lang/it.yml
+++ b/lang/it.yml
@@ -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
diff --git a/lang/ja.yml b/lang/ja.yml
index e8f41d926..d8dcc61db 100644
--- a/lang/ja.yml
+++ b/lang/ja.yml
@@ -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点の変更
diff --git a/lang/nl.yml b/lang/nl.yml
index 29aca8b33..3824925ac 100644
--- a/lang/nl.yml
+++ b/lang/nl.yml
@@ -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
diff --git a/lang/pt-br.yml b/lang/pt-br.yml
index ea6ad5deb..658d23696 100644
--- a/lang/pt-br.yml
+++ b/lang/pt-br.yml
@@ -167,7 +167,7 @@ setting_host_name: Servidor
setting_text_formatting: Formato do texto
setting_wiki_compression: Compactacao do historio do Wiki
setting_feeds_limit: Limite do Feed
-setting_autofetch_changesets: Autofetch SVN commits
+setting_autofetch_changesets: Autofetch commits
setting_sys_api_enabled: Ativa WS para gerenciamento do repositorio
setting_commit_ref_keywords: Referencing keywords
setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 @@ label_ago: dias atras
label_contains: contem
label_not_contains: nao contem
label_day_plural: dias
-label_repository: SVN Repository
+label_repository: Repository
label_browse: Browse
label_modification: %d change
label_modification_plural: %d changes
diff --git a/lang/pt.yml b/lang/pt.yml
index 71b5fe077..9d71a4af6 100644
--- a/lang/pt.yml
+++ b/lang/pt.yml
@@ -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
diff --git a/lang/sv.yml b/lang/sv.yml
index dffa86777..9d53e1b87 100644
--- a/lang/sv.yml
+++ b/lang/sv.yml
@@ -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
diff --git a/lang/zh.yml b/lang/zh.yml
index bd40141d3..e317afa20 100644
--- a/lang/zh.yml
+++ b/lang/zh.yml
@@ -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 个更新
diff --git a/lib/redmine.rb b/lib/redmine.rb
index 553ea7f5b..76edeca55 100644
--- a/lib/redmine.rb
+++ b/lib/redmine.rb
@@ -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
index 000000000..b74fa1b18
--- /dev/null
+++ b/lib/redmine/scm/adapters/abstract_adapter.rb
@@ -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
index 000000000..c5f85f1c6
--- /dev/null
+++ b/lib/redmine/scm/adapters/cvs_adapter.rb
@@ -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
index 000000000..54fa8c4f8
--- /dev/null
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -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
index 000000000..f58bdb13d
--- /dev/null
+++ b/lib/redmine/scm/adapters/subversion_adapter.rb
@@ -0,0 +1,173 @@
+# 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'
+require 'rexml/document'
+
+module Redmine
+ module Scm
+ module Adapters
+ class SubversionAdapter < AbstractAdapter
+
+ # SVN executable name
+ SVN_BIN = "svn"
+
+ # Get info about the svn repository
+ def info
+ cmd = "#{SVN_BIN} 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_BIN} 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_BIN} 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_BIN} 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_BIN} 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
+ end
+ end
+ end
+end
diff --git a/test/unit/repository_test.rb b/test/unit/repository_test.rb
index fd4fb6737..e420b6452 100644
--- a/test/unit/repository_test.rb
+++ b/test/unit/repository_test.rb
@@ -25,7 +25,7 @@ class RepositoryTest < Test::Unit::TestCase
end
def test_create
- repository = Repository.new(:project => Project.find(2))
+ repository = Repository::Subversion.new(:project => Project.find(2))
assert !repository.save
repository.url = "svn://localhost"
@@ -34,12 +34,6 @@ class RepositoryTest < Test::Unit::TestCase
project = Project.find(2)
assert_equal repository, project.repository
- end
-
- 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