From: Jean-Philippe Lang Date: Tue, 12 Jun 2007 20:12:05 +0000 (+0000) Subject: Added basic support for CVS and Mercurial SCMs. X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=438161ad1fd37aadbfa3f5a875540730fd6d70c3;p=redminele%2Fredmine.git 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 --- diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 0dc6cba2..ead1a224 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 4252e58b..21e1997a 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 12302a07..564a9938 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 2c7dcdd5..e2058a71 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 692c446d..667ef5ef 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 00000000..d6d4ed31 --- /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 00000000..5d9ea9cd --- /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 00000000..cc9c975a --- /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 45d0b289..00000000 --- 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 5f253d40..9eb93303 100644 --- a/app/views/projects/_form.rhtml +++ b/app/views/projects/_form.rhtml @@ -27,17 +27,17 @@ -

<%= check_box_tag "repository_enabled", 1, !@project.repository.nil?, :onclick => "Element.toggle('repository');" %> <%= l(:label_repository) %>

-<%= hidden_field_tag "repository_enabled", 0 %> -
-<% fields_for :repository, @project.repository, { :builder => TabularFormBuilder, :lang => current_language} do |repository| %> -

<%= 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 %> +
+

<%= check_box_tag "repository_enabled", 1, !@project.repository.nil?, :onclick => "Element.toggle('repository');" %> <%= l(:label_repository) %>

+ <%= hidden_field_tag "repository_enabled", 0 %> +
+

<%= scm_select_tag %>

+
+ <%= render :partial => 'projects/repository', :locals => {:repository => @project.repository} if @project.repository %> +
+
<%= javascript_tag "Element.hide('repository');" if @project.repository.nil? %> -

<%= check_box_tag "wiki_enabled", 1, !@project.wiki.nil?, :onclick => "Element.toggle('wiki');" %> <%= l(:label_wiki) %>

@@ -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 00000000..6f79c615 --- /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 0e5b712b..5555ee87 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| %> -<%= 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')) %> -<%= number_to_human_size(entry.size) unless entry.is_dir? %> -<%= link_to entry.lastrev.identifier, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier %> -<%= format_time(entry.lastrev.time) %> -<%=h entry.lastrev.author %> -<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) %> +<%= 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')) %> +<%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %> +<%= link_to(entry.lastrev.name, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %> +<%= format_time(entry.lastrev.time) if entry.lastrev %> +<%=h(entry.lastrev.author) if entry.lastrev %> +<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %> <%=h truncate(changeset.comments, 100) unless changeset.nil? %> -<% total_size += entry.size +<% total_size += entry.size if entry.size end %> diff --git a/app/views/repositories/_navigation.rhtml b/app/views/repositories/_navigation.rhtml index efc0b9ff..823b4f44 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 faec1666..b2bdb6c7 100644 --- a/app/views/repositories/_revisions.rhtml +++ b/app/views/repositories/_revisions.rhtml @@ -9,12 +9,13 @@ <%= l(:field_comments) %> -<% 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 %> -<%= link_to changeset.revision, :action => 'revision', :id => project, :rev => changeset.revision %> -<%= 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) %> +<%= link_to (revision.revision || changeset.revision), :action => 'revision', :id => project, :rev => changeset.revision %> +<%= 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) %> <%= 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) %> <%= format_time(changeset.committed_on) %> <%=h changeset.committer %> diff --git a/app/views/repositories/changes.rhtml b/app/views/repositories/changes.rhtml new file mode 100644 index 00000000..35ce939f --- /dev/null +++ b/app/views/repositories/changes.rhtml @@ -0,0 +1,13 @@ +

<%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %>

+ +

<%=h @entry.name %>

+ +

+<% 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 5cf5c2e4..b484becc 100644 --- a/app/views/repositories/revision.rhtml +++ b/app/views/repositories/revision.rhtml @@ -7,7 +7,9 @@

<%= l(:label_revision) %> <%= @changeset.revision %>

-

<%= @changeset.committer %>, <%= format_time(@changeset.committed_on) %>

+

<% if @changeset.scmid %>ID: <%= @changeset.scmid %>
<% end %> +<%= @changeset.committer %>, <%= format_time(@changeset.committed_on) %>

+ <%= textilizable @changeset.comments %> <% if @changeset.issues.any? %> @@ -30,7 +32,7 @@ <% @changes.each do |change| %> -
<%= change.path %> +
<%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %> <% 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 4a5b3766..0c2655d5 100644 --- a/app/views/repositories/revisions.rhtml +++ b/app/views/repositories/revisions.rhtml @@ -5,25 +5,13 @@ <% end %>
-

<%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %>

+

<%= l(:label_revision_plural) %>

-<% if @entry && @entry.is_file? %> -

<%=h @entry.name %>

-

-<% 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 %> - -

<%= l(:label_revision_plural) %>

- -<%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :changesets => @changesets, :entry => @entry }%> +<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%>

<%= 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 04a58b4c..fcf95447 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' %>
-

<%= l(:label_repository) %>

+

<%= l(:label_repository) %> (<%= @repository.scm_name %>)

+<% unless @entries.nil? %>

<%= l(:label_browse) %>

<%= render :partial => 'dir_list' %> +<% end %> <% unless @changesets.empty? %>

<%= l(:label_latest_revision_plural) %>

-<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :changesets => @changesets, :entry => nil }%> +<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%>

<%= 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 00000000..6f58c1a7 --- /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 00000000..998ce2ba --- /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 00000000..188fa6ef --- /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 00000000..599f70aa --- /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 00000000..e9351490 --- /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 6b962649..71d4759a 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 feb3ca83..f2965b35 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 2fa6ddab..1cc12605 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 4b39c345..484a947e 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 822e39da..d0facf0d 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 20ec6354..a68c4102 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 e8f41d92..d8dcc61d 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 29aca8b3..3824925a 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 ea6ad5de..658d2369 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 71b5fe07..9d71a4af 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 dffa8677..9d53e1b8 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 bd40141d..e317afa2 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 553ea7f5..76edeca5 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 00000000..b74fa1b1 --- /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 00000000..c5f85f1c --- /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 " 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(" #{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 " 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 " 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 " 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 " 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 00000000..54fa8c4f --- /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 00000000..f58bdb13 --- /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 fd4fb673..e420b645 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