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-8f06a7374b81tags/0.5.1
@@ -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] |
@@ -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 |
@@ -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)) |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 %> | |||
<% end %> |
@@ -0,0 +1,3 @@ | |||
<% fields_for :repository, repository, { :builder => TabularFormBuilder, :lang => current_language} do |f| %> | |||
<%= repository_field_tags(f, repository) %> | |||
<% end %> |
@@ -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> |
@@ -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 %> | |||
<%= "@ #{revision}" if revision %> |
@@ -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> |
@@ -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 }%> |
@@ -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 %> |
@@ -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 %> | |||
<% end %> |
@@ -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 %> | |||
<% end %> |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 промени |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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点の変更 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 个更新 |
@@ -1,3 +1,5 @@ | |||
require 'redmine/version' | |||
require 'redmine/mime_type' | |||
require 'redmine/acts_as_watchable/init' | |||
REDMINE_SUPPORTED_SCM = %w( Subversion Mercurial Cvs ) |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |