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