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
- # get entries for the browse frame
- @entries = @repository.scm.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")
+ @repository.fetch_changesets if Setting.autofetch_changesets?
+ # get entries for the browse frame
+ @entries = @repository.entries('')
+ show_error and return unless @entries
+ # 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?)) +
+ '
(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/, ' ')
- 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 @@
-
<%= repository.text_field :url, :size => 60, :required => true, :disabled => (@project.repository && !@project.repository.root_url.blank?) %>
(http://, https://, svn://, file:///)
<%= repository.text_field :login, :size => 30 %>
-<%= repository.password_field :password, :size => 30 %>
-<% end %> +<%= scm_select_tag %>
++<% 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 %> +
+ +<%= 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 @@<%= @changeset.committer %>, <%= format_time(@changeset.committed_on) %>
+<% if @changeset.scmid %>ID: <%= @changeset.scmid %>
<% end %>
+<%= @changeset.committer %>, <%= format_time(@changeset.committed_on) %>
-<% 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 %>)
-<% end %> - -<%= pagination_links_full @changeset_pages %> [ <%= @changeset_pages.current.first_item %> - <%= @changeset_pages.current.last_item %> / <%= @changeset_count %> ]
<% 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' %> -<%= link_to l(:label_view_revisions), :action => 'revisions', :id => @project %>
<% 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/, ' ') + 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 "