Repository Management - Refactoring and Preparation

Related work package: [#20415](https://community.openproject.org/work_packages/20415).

This is a combined refactoring and extraction effort of local SCM functionality for SVM, Git adapters in preparation for the explicit lifetime management of repositories in #20416 and implementation of (local) repository storage quota in #18393.

This PR is still heavily WIP and for the time being, depends on the **legacy specs** for testing.
To run the tests, execute the following steps:

~~~
rake test:scm:setup:all
rake spec:legacy
~~~

I'm open for ideas on how we integrate scm adapters tests into current specs.

---

- [x] Exchange `IO::Open` for `Open3` to avoid countless manual exitcode checks
- [x] Extract local functionality in its own Module
  - [] Extend `LocalClient` module with links to create/delete services for #20415
- [x] Break down unbearable local parse routines (Hound will be over the place until fixed - but down from 260 to < 20)
  - [x] `entries`
  - [x] `revisions`
- [] local repository factory
- [] Specs
  - [] Fix / Modernize
  - [] More adapter-related specs
pull/3196/head
Oliver Günther 10 years ago
parent 4a155d267e
commit 7ebac37c14
  1. 5
      app/controllers/application_controller.rb
  2. 33
      app/controllers/repositories_controller.rb
  3. 2
      app/helpers/repositories_helper.rb
  4. 8
      app/models/repository.rb
  5. 4
      app/models/repository/filesystem.rb
  6. 4
      app/models/repository/git.rb
  7. 4
      app/models/repository/subversion.rb
  8. 71
      app/services/scm/create_repository_service.rb
  9. 8
      app/views/repositories/_dir_list_content.html.erb
  10. 2
      app/views/settings/_repositories.html.erb
  11. 8
      config/initializers/scm.rb
  12. 165
      lib/open_project/scm/adapters.rb
  13. 160
      lib/open_project/scm/adapters/base.rb
  14. 47
      lib/open_project/scm/adapters/filesystem.rb
  15. 371
      lib/open_project/scm/adapters/git.rb
  16. 163
      lib/open_project/scm/adapters/local_client.rb
  17. 282
      lib/open_project/scm/adapters/subversion.rb
  18. 4
      lib/open_project/scm/manager.rb
  19. 365
      lib/redmine/scm/adapters/abstract_adapter.rb
  20. 371
      lib/redmine/scm/adapters/git_adapter.rb
  21. 293
      lib/redmine/scm/adapters/subversion_adapter.rb
  22. 2
      spec/legacy/functional/repositories_subversion_controller_spec.rb
  23. 4
      spec/legacy/unit/lib/redmine/scm/adapters/filesystem_adapter_spec.rb
  24. 12
      spec/legacy/unit/lib/redmine/scm/adapters/git_adapter_spec.rb
  25. 10
      spec/legacy/unit/lib/redmine/scm/adapters/subversion_adapter_spec.rb
  26. 6
      spec/lib/redmine/scm/base_spec.rb

@ -118,11 +118,6 @@ class ApplicationController < ActionController::Base
include Redmine::MenuManager::MenuController
helper Redmine::MenuManager::MenuHelper
# TODO: needed? redmine doesn't
Redmine::Scm::Base.all.each do |scm|
require "repository/#{scm.underscore}"
end
def default_url_options(_options = {})
{ layout: params['layout'] }
end

@ -30,6 +30,7 @@
require 'SVG/Graph/Bar'
require 'SVG/Graph/BarHorizontal'
require 'digest/sha1'
require_dependency 'open_project/scm/adapters'
class ChangesetNotFound < Exception; end
class InvalidRevisionParam < Exception; end
@ -46,14 +47,11 @@ class RepositoriesController < ApplicationController
before_filter :authorize
accept_key_auth :revisions
rescue_from Redmine::Scm::Adapters::CommandFailed, with: :show_error_command_failed
rescue_from OpenProject::Scm::Adapters::CommandFailed, with: :show_error_command_failed
def edit
@repository = @project.repository
if !@repository
@repository = Repository.factory(params[:repository_scm])
@repository.project = @project if @repository
end
@repository = @project.repository || build_repository
if request.post? && @repository
@repository.attributes = params[:repository]
@repository.save
@ -138,7 +136,10 @@ class RepositoriesController < ApplicationController
(show_error_not_found; return) unless @entry
# If the entry is a dir, show the browser
(show; return) if @entry.is_dir?
if @entry.dir?
show
return
end
@content = @repository.cat(@path, @rev)
(show_error_not_found; return) unless @content
@ -276,11 +277,27 @@ class RepositoriesController < ApplicationController
show_error_not_found
end
def build_repository
repository = Repository.factory(params[:repository_scm])
if repository.nil?
flash[:error] = l(:repository_factory_error)
redirect_to controller: '/projects', action: 'settings', id: @project, tab: 'repository'
end
repository.project = @project
if params[:managed_epository] && CreateRepositoryServce.allowed?(repository)
CreateRepositoryService.new(repository)
end
repository
end
def show_error_not_found
render_error message: l(:error_scm_not_found), status: 404
end
# Handler for Redmine::Scm::Adapters::CommandFailed exception
# Handler for OpenProject::Scm::Adapters::CommandFailed exception
def show_error_command_failed(exception)
render_error l(:error_scm_command_failed, exception.message)
end

@ -185,7 +185,7 @@ module RepositoriesHelper
def scm_select_tag(repository)
scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
Redmine::Scm::Base.configured.each do |scm|
OpenProject::Scm::Manager.configured.each do |scm|
if Setting.enabled_scm.include?(scm) ||
(repository && repository.class.name.demodulize == scm)
scm_options << ["Repository::#{scm}".constantize.scm_name, scm]

@ -235,7 +235,7 @@ class Repository < ActiveRecord::Base
if project.repository
begin
project.repository.fetch_changesets
rescue Redmine::Scm::Adapters::CommandFailed => e
rescue OpenProject::Scm::Adapters::CommandFailed => e
logger.error "scm: error during fetching changesets: #{e.message}"
end
end
@ -270,7 +270,7 @@ class Repository < ActiveRecord::Base
ret = ''
begin
ret = scm_adapter_class.client_command if scm_adapter_class
rescue Redmine::Scm::Adapters::CommandFailed => e
rescue OpenProject::Scm::Adapters::CommandFailed => e
logger.error "scm: error during get command: #{e.message}"
end
ret
@ -280,7 +280,7 @@ class Repository < ActiveRecord::Base
ret = ''
begin
ret = scm_adapter_class.client_version_string if scm_adapter_class
rescue Redmine::Scm::Adapters::CommandFailed => e
rescue OpenProject::Scm::Adapters::CommandFailed => e
logger.error "scm: error during get version string: #{e.message}"
end
ret
@ -290,7 +290,7 @@ class Repository < ActiveRecord::Base
ret = false
begin
ret = scm_adapter_class.client_available if scm_adapter_class
rescue Redmine::Scm::Adapters::CommandFailed => e
rescue OpenProject::Scm::Adapters::CommandFailed => e
logger.error "scm: error during get scm available: #{e.message}"
end
ret

@ -27,7 +27,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'redmine/scm/adapters/filesystem_adapter'
require 'open_project/scm/adapters/filesystem'
class Repository::Filesystem < Repository
attr_protected :root_url
@ -44,7 +44,7 @@ class Repository::Filesystem < Repository
end
def self.scm_adapter_class
Redmine::Scm::Adapters::FilesystemAdapter
OpenProject::Scm::Adapters::FileSystem
end
def self.scm_name

@ -27,7 +27,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'redmine/scm/adapters/git_adapter'
require 'open_project/scm/adapters/git'
class Repository::Git < Repository
attr_protected :root_url
@ -41,7 +41,7 @@ class Repository::Git < Repository
end
def self.scm_adapter_class
Redmine::Scm::Adapters::GitAdapter
OpenProject::Scm::Adapters::Git
end
def self.scm_name

@ -27,7 +27,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'redmine/scm/adapters/subversion_adapter'
require 'open_project/scm/adapters/subversion'
class Repository::Subversion < Repository
attr_protected :root_url
@ -35,7 +35,7 @@ class Repository::Subversion < Repository
validates_format_of :url, with: /\A(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+\z/i
def self.scm_adapter_class
Redmine::Scm::Adapters::SubversionAdapter
OpenProject::Scm::Adapters::Subversion
end
def self.scm_name

@ -0,0 +1,71 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
##
# Implements the asynchronous creation of a local repository.
class DeleteUserService < Struct.new :repository
##
# Deletes the given user if allowed.
#
# @return True if the user deletion has been initiated, false otherwise.
def call
if deletion_allowed?
# as destroying users is a lengthy process we handle it in the background
# and lock the account now so that no action can be performed with it
user.lock!
Delayed::Job.enqueue DeleteUserJob.new(user)
logout! if self_delete?
true
else
false
end
end
##
# Checks if a given repository may be created and managed locally.
#
# @param repository [Repository] SCM repository to be created
def self.allowed?(repository)
enabled = config[:git]
if repository.managed_by_openproject?
Setting.users_deletable_by_self?
else
actor.admin && Setting.users_deletable_by_admins?
end
end
private
def self.config
OpenProject::Configuration[:scm].presence || {}
end
end

@ -33,7 +33,7 @@ See doc/COPYRIGHT.rdoc for more details.
<% ent_name = replace_invalid_utf8(entry.name) %>
<tr id="<%= tr_id %>" class="<%= h params[:parent_id] %> entry <%= h(entry.kind) %>">
<td style="padding-left: <%=18 * depth%>px;" class="filename">
<% if entry.is_dir? %>
<% if entry.dir? %>
<span class="icon-context dir-expander" onclick="<%= remote_function :url => {:action => 'show', :project_id => @project, :path => to_path_param(ent_path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
:method => :get,
:update => { :success => tr_id },
@ -42,10 +42,10 @@ See doc/COPYRIGHT.rdoc for more details.
:condition => "scmEntryClick('#{tr_id}')"%>">&nbsp</span>
<% end %>
<%= link_to h(ent_name),
{:action => (entry.is_dir? ? 'show' : 'changes'), :project_id => @project, :path => to_path_param(ent_path), :rev => @rev},
:class => (entry.is_dir? ? 'icon-context icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
{:action => (entry.dir? ? 'show' : 'changes'), :project_id => @project, :path => to_path_param(ent_path), :rev => @rev},
:class => (entry.dir? ? 'icon-context icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
</td>
<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.dir? %></td>
<% changeset = @project.repository.find_changeset_by_name(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
<td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td>
<td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>

@ -39,7 +39,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= link_to_function l(:label_generate_key), "if ($('settings_sys_api_key').disabled == false) { $('settings_sys_api_key').value = randomKey(20) }" %>
</span>
</div>
<div class="form--field"><%= setting_multiselect(:enabled_scm, Redmine::Scm::Base.configured) %></div>
<div class="form--field"><%= setting_multiselect(:enabled_scm, OpenProject::Scm::Manager.configured) %></div>
<div class="form--field">
<%= setting_text_field :repositories_encodings, :size => 60 %>
<div class="form--field-instructions"><%= l(:text_comma_separated) %></div>

@ -27,8 +27,8 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'redmine/scm/base'
require 'open_project/scm/manager'
Redmine::Scm::Base.add 'Subversion'
Redmine::Scm::Base.add 'Git'
Redmine::Scm::Base.add 'Filesystem'
OpenProject::Scm::Manager.add 'Subversion'
OpenProject::Scm::Manager.add 'Git'
OpenProject::Scm::Manager.add 'Filesystem'

@ -0,0 +1,165 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject
module Scm
module Adapters
class CommandFailed < StandardError
attr_reader :program
attr_reader :command
attr_reader :message
# Create a +CommandFailed+ exception for the executed program (e.g., 'svn'),
# the executed full command string (e.g., 'svn info --xml ...'),
# and a meaningful error message
#
# If the operation throws an exception or the operation we rethrow a
# +ShellError+ with a meaningful error message.
def initialize(program, command, message)
@program = program
@command = command
@message = message
end
def to_s
"CommandFailed(#{@program}) -> #{@message}"
end
end
# raised if scm command exited with error, e.g. unknown revision.
class ScmCommandAborted < CommandFailed
end
class Entries < Array
def sort_by_name
sort do |x, y|
if x.kind == y.kind
x.name.to_s <=> y.name.to_s
else
x.kind <=> y.kind
end
end
end
def revisions
Revisions.new(map(&: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 = {})
[:name, :path, :kind, :size].each do |attr|
send("#{attr}=", attributes[attr])
end
self.size = size.to_i if size.present?
self.lastrev = attributes[:lastrev]
end
def file?
'file' == kind
end
def dir?
'dir' == kind
end
def text?
Redmine::MimeType.is_type?('text', name)
end
end
class Revisions < Array
def latest
sort do |x, y|
if x.time.nil? or y.time.nil?
0
else
x.time <=> y.time
end
end.last
end
end
class Revision
attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch
attr_writer :identifier
def initialize(attributes = {})
[:identifier, :scmid, :author, :time, :paths, :revision, :branch].each do |attr|
send("#{attr}=", attributes[attr])
end
self.name = attributes[:name].presence || identifier
self.message = attributes[:message].presence || ''
end
# Returns the identifier of this revision; see also Changeset model
def identifier
(@identifier || revision).to_s
end
# Returns the readable identifier.
def format_identifier
identifier
end
end
class Annotate
attr_reader :lines, :revisions
def initialize
@lines = []
@revisions = []
end
def add_line(line, revision)
@lines << line
@revisions << revision
end
def content
lines.join("\n")
end
def empty?
lines.empty?
end
end
end
end
end

@ -0,0 +1,160 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'open_project/scm/adapters'
module OpenProject
module Scm
module Adapters
class Base
def initialize(url, root_url = nil)
@url = url
@root_url = root_url.blank? ? retrieve_root_url : root_url
end
def local?
false
end
def remote?
!local?
end
def logger
Rails.logger
end
def adapter_name
'Base'
end
def root_url
@root_url
end
def url
@url
end
def info
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)
parts = path.to_s.split(%r{[\/\\]}).select { |n| !n.blank? }
search_entries(parts, identifier)
end
def search_entries(parts, identifier)
search_path = parts[0..-2].join('/')
search_name = parts[-1]
if search_path.blank? && search_name.blank?
# Root entry
Entry.new(path: '', kind: 'dir')
else
# Search for the entry in the parent directory
es = entries(search_path, identifier)
es ? es.detect { |e| e.name == search_name } : nil
end
end
# Returns an Entries collection
# or nil if the given path doesn't exist in the repository
def entries(_path = nil, _identifier = nil)
nil
end
def branches
nil
end
def tags
nil
end
def default_branch
nil
end
def properties(_path, _identifier = nil)
nil
end
def revisions(_path = nil, _identifier_from = nil, _identifier_to = nil, _options = {})
nil
end
def diff(_path, _identifier_from, _identifier_to = nil)
nil
end
def cat(_path, _identifier = nil)
nil
end
def with_leading_slash(path)
path ||= ''
(path[0, 1] != '/') ? "/#{path}" : path
end
def with_trailling_slash(path)
path ||= ''
(path[-1, 1] == '/') ? path : "#{path}/"
end
def without_leading_slash(path)
path ||= ''
path.gsub(%r{\A/+}, '')
end
def without_trailling_slash(path)
path ||= ''
(path[-1, 1] == '/') ? path[0..-2] : path
end
def retrieve_root_url
info = self.info
info ? info.root_url : nil
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
end
end
end

@ -27,37 +27,28 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'redmine/scm/adapters/abstract_adapter'
require 'find'
module Redmine
module OpenProject
module Scm
module Adapters
class FilesystemAdapter < AbstractAdapter
class << self
def client_available
true
end
end
class FileSystem < Base
include LocalClient
def initialize(url, _root_url = nil, _login = nil, _password = nil,
path_encoding = nil)
def initialize(url, _root_url = nil, _login = nil, _password = nil, path_encoding = nil)
@url = with_trailling_slash(url)
@path_encoding = path_encoding || 'UTF-8'
end
def format_path_ends(path, leading = true, trailling = true)
path = leading ? with_leading_slash(path) :
without_leading_slash(path)
trailling ? with_trailling_slash(path) :
without_trailling_slash(path)
path = leading ? with_leading_slash(path) : without_leading_slash(path)
trailling ? with_trailling_slash(path) : without_trailling_slash(path)
end
def info
info = Info.new(root_url: target,
lastrev: nil
)
info
Info.new(root_url: target,
lastrev: nil
)
rescue CommandFailed
return nil
end
@ -81,20 +72,20 @@ module Redmine
p1 = File.readable?(t1) ? relative_path : ''
utf_8_path = scm_encode('UTF-8', @path_encoding, p1)
entries <<
Entry.new(name: scm_encode('UTF-8', @path_encoding, File.basename(e1)),
# below : list unreadable files, but dont link them.
path: utf_8_path,
kind: (File.directory?(t1) ? 'dir' : 'file'),
size: (File.directory?(t1) ? nil : [File.size(t1)].pack('l').unpack('L').first),
lastrev:
Revision.new(time: (File.mtime(t1)))
)
Entry.new(
name: scm_encode('UTF-8', @path_encoding, File.basename(e1)),
# below : list unreadable files, but dont link them.
path: utf_8_path,
kind: (File.directory?(t1) ? 'dir' : 'file'),
size: (File.directory?(t1) ? nil : [File.size(t1)].pack('l').unpack('L').first),
lastrev: Revision.new(time: (File.mtime(t1)))
)
end
end
entries.sort_by_name
rescue => err
logger.error "scm: filesystem: error: #{err.message}"
raise CommandFailed.new(err.message)
raise CommandFailed.new('filesystem', '', err.message)
end
def cat(path, _identifier = nil)
@ -102,7 +93,7 @@ module Redmine
File.new(p, 'rb').read
rescue => err
logger.error "scm: filesystem: error: #{err.message}"
raise CommandFailed.new(err.message)
raise CommandFailed.new('filesystem', '', err.message)
end
private

@ -0,0 +1,371 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require_dependency 'open_project/scm/adapters'
module OpenProject
module Scm
module Adapters
class Git < Base
include LocalClient
SCM_GIT_REPORT_LAST_COMMIT = true
def initialize(url, root_url = nil, _login = nil, _password = nil, path_encoding = nil)
super(url, root_url)
@flag_report_last_commit = SCM_GIT_REPORT_LAST_COMMIT
@path_encoding = path_encoding.presence || 'UTF-8'
end
def client_command
@client_command ||= scm_config[:git_client_command]|| 'git'
end
def client_version
@client_version ||= (git_binary_version || [])
end
def scm_version_from_command_line
capture_out(%w[--version --no-color])
end
def git_binary_version
scm_version = scm_version_from_command_line.dup
if scm_version.respond_to?(:force_encoding)
scm_version.force_encoding('ASCII-8BIT')
end
m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
unless m.nil?
m[2].scan(%r{\d+}).map(&:to_i)
end
end
def info
Info.new(root_url: url, lastrev: lastrev('', nil))
end
def branches
return @branches if @branches
@branches = []
cmd_args = %w|branch --no-color|
popen3(cmd_args) do |io|
io.each_line do |line|
@branches << line.match('\s*\*?\s*(.*)$')[1]
end
end
@branches.sort!
end
def tags
return @tags if @tags
cmd_args = %w|tag|
@tags = capture_git(cmd_args).lines.sort!.map(&:strip)
end
def default_branch
bras = branches
return nil if bras.nil?
bras.include?('master') ? 'master' : bras.first
end
def entries(path, identifier = nil)
entries = Entries.new
path = scm_encode(@path_encoding, 'UTF-8', path)
args = %w|ls-tree -l|
args << "HEAD:#{path}" if identifier.nil?
args << "#{identifier}:#{path}" if identifier
parse_by_line(args, binmode: true) do |line|
e = parse_entry(line, path, identifier)
entries << e unless entries.detect { |entry| entry.name == e.name }
end
entries.sort_by_name
end
def parse_entry(line, path, identifier)
if line.chomp =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
type = $1
size = $3
name = $4.force_encoding(@path_encoding)
path = encode_full_path(name, path)
Entry.new(
name: scm_encode('UTF-8', @path_encoding, name),
path: path,
kind: (type == 'tree') ? 'dir' : 'file',
size: (type == 'tree') ? nil : size,
lastrev: @flag_report_last_commit ? lastrev(path, identifier) : Revision.new
)
end
end
def encode_full_path(name, path)
full_path = path.empty? ? name : "#{path}/#{name}"
scm_encode('UTF-8', @path_encoding, full_path)
end
def lastrev(path, rev)
return nil if path.nil?
args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
args << rev if rev
args << '--' << path unless path.empty?
lines = capture_git(args).lines
begin
build_lastrev(lines)
rescue NoMethodError
logger.error("The revision '#{path}' has a wrong format")
return nil
end
end
def build_lastrev(lines)
id = lines[0].split[1]
author = lines[1].match('Author:\s+(.*)$')[1]
time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
Revision.new(
identifier: id,
scmid: id,
author: author,
time: time,
message: nil,
paths: nil
)
end
def revisions(path, identifier_from, identifier_to, options = {})
revisions = Revisions.new
args = build_revision_args(path, identifier_from, identifier_to, options)
files = []
changeset = {}
parsing_descr = 0 # 0: not parsing desc or files, 1: parsing desc, 2: parsing files
parse_by_line(args, binmode: true) do |line|
if line =~ /^commit ([0-9a-f]{40})$/
key = 'commit'
value = $1
if parsing_descr == 1 || parsing_descr == 2
parsing_descr = 0
revision = Revision.new(
identifier: changeset[:commit],
scmid: changeset[:commit],
author: changeset[:author],
time: Time.parse(changeset[:date]),
message: changeset[:description],
paths: files
)
if block_given?
yield revision
else
revisions << revision
end
changeset = {}
files = []
end
changeset[:commit] = $1
elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
key = $1
value = $2
if key == 'Author'
changeset[:author] = value
elsif key == 'CommitDate'
changeset[:date] = value
end
elsif (parsing_descr == 0) && line.chomp.to_s == ''
parsing_descr = 1
changeset[:description] = ''
elsif (parsing_descr == 1 || parsing_descr == 2) &&
(line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/)
parsing_descr = 2
fileaction = $1
filepath = $2
p = scm_encode('UTF-8', @path_encoding, filepath)
files << { action: fileaction, path: p }
elsif (parsing_descr == 1 || parsing_descr == 2) &&
(line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/)
parsing_descr = 2
fileaction = $1
filepath = $3
p = scm_encode('UTF-8', @path_encoding, filepath)
files << { action: fileaction, path: p }
elsif (parsing_descr == 1) && line.chomp.to_s == ''
parsing_descr = 2
elsif parsing_descr == 1
changeset[:description] << line[4..-1]
end
end
if changeset[:commit]
revision = Revision.new(
identifier: changeset[:commit],
scmid: changeset[:commit],
author: changeset[:author],
time: Time.parse(changeset[:date]),
message: changeset[:description],
paths: files
)
if block_given?
yield revision
else
revisions << revision
end
end
revisions
end
def build_revision_args(path, identifier_from, identifier_to, options)
args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller|
args << '--reverse' if options[:reverse]
args << '--all' if options[:all]
args << '-n' << "#{options[:limit].to_i}" if options[:limit]
from_to = ''
from_to << "#{identifier_from}.." if identifier_from
from_to << "#{identifier_to}" if identifier_to
args << from_to if from_to.present?
args << "--since=#{options[:since].strftime('%Y-%m-%d %H:%M:%S')}" if options[:since]
args << '--' << scm_encode(@path_encoding, 'UTF-8', path) if path && !path.empty?
args
end
def diff(path, identifier_from, identifier_to = nil)
args = []
if identifier_to
args << 'diff' << '--no-color' << identifier_to << identifier_from
else
args << 'show' << '--no-color' << identifier_from
end
args << '--' << scm_encode(@path_encoding, 'UTF-8', path) unless path.empty?
capture_git(args).lines.map(&:chomp)
rescue CommandFailed
nil
end
def annotate(path, identifier = nil)
identifier = 'HEAD' if identifier.blank?
args = %w|blame|
args << '-p' << identifier << '--' << scm_encode(@path_encoding, 'UTF-8', path)
blame = Annotate.new
content = capture_git(args, binmode: true)
if content.respond_to?(:force_encoding) &&
(content.dup.force_encoding('UTF-8') != content.dup.force_encoding('BINARY'))
# Ruby 1.9.2
# TODO: need to handle edge cases of non-binary content that isn't UTF-8
return nil
end
identifier = ''
# git shows commit author on the first occurrence only
authors_by_commit = {}
content.split("\n").each do |line|
if line =~ /^([0-9a-f]{39,40})\s.*/
identifier = $1
elsif line =~ /^author (.+)/
authors_by_commit[identifier] = $1.strip
elsif line =~ /^\t(.*)/
blame.add_line($1, Revision.new(
identifier: identifier,
author: authors_by_commit[identifier]))
identifier = ''
end
end
blame
end
def cat(path, identifier = nil)
if identifier.nil?
identifier = 'HEAD'
end
args = %w|show --no-color|
args << "#{identifier}:#{scm_encode(@path_encoding, 'UTF-8', path)}"
capture_git(args, binmode: true)
end
class Revision < OpenProject::Scm::Adapters::Revision
# Returns the readable identifier
def format_identifier
identifier[0, 8]
end
end
private
##
# Builds the full git arguments from the parameters
# and return the executed stdout as a string
def capture_git(args, opt = {})
cmd = build_git_cmd(args)
capture_out(cmd, opt)
end
##
# Builds the full git arguments from the parameters
# and calls the given block with in, out, err, thread
# from +Open3#popen3+.
def popen3(args, opt = {}, &block)
cmd = build_git_cmd(args)
super(cmd, opt) do |_stdin, stdout, _stderr, wait_thr|
block.call(stdout)
process = wait_thr.value
if process.exitstatus != 0
raise CommandFailed, "git exited with non-zero status: #{$?.exitstatus}"
end
end
end
##
# Runs the given arguments through git
# and processes the result line by line.
#
def parse_by_line(cmd, opts = {}, &block)
popen3(cmd) do |io|
io.binmode if opts[:binmode]
io.each_line &block
end
end
def build_git_cmd(args)
if client_version_above?([1, 7, 2])
args.unshift('-c', 'core.quotepath=false')
end
args.unshift('--git-dir', (root_url || url))
end
end
end
end
end

@ -0,0 +1,163 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'open3'
module OpenProject
module Scm
module Adapters
module LocalClient
##
# Determines local capabilities for SCM creation.
# Overridden by including classes when SCM may be remote.
def local?
true
end
##
# Reads the configuration for this strategy from OpenProject's `configuration.yml`.
def scm_config
OpenProject::Configuration[:scm]
%w(scm global_basic_auth).inject(config) do |acc, key|
HashWithIndifferentAccess.new acc[key]
end
end
##
# client executable command
def client_command
''
end
def client_available
!client_version.empty?
end
##
# Returns the version of the scm client
# Eg: [1, 5, 0] or [] if unknown
def client_version
[]
end
##
# Returns the version string of the scm client
# Eg: '1.5.0' or 'Unknown version' if unknown
def client_version_string
v = client_version || 'Unknown version'
v.is_a?(Array) ? v.join('.') : v.to_s
end
##
# Returns true if the current client version is above
# or equals the given one
# If option is :unknown is set to true, it will return
# true if the client version is unknown
def client_version_above?(v, options = {})
((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
end
def shell_quote(str)
Shellwords.escape(str)
end
def supports_cat?
true
end
def supports_annotate?
respond_to?('annotate')
end
def target(path = '')
base = path.match(/\A\//) ? root_url : url
shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
end
# Executes the given arguments for +client_command+ on the shell
# and returns the resulting stdout.
#
# May optionally specify an opts hash with flags for popen3 and Process.spawn
# (cf., :binmode, :stdin_data in +Open3.capture3+)
#
# If the operation throws an exception or the operation yields a non-zero exit code
# we rethrow a +CommandFailed+ with a meaningful error message
def capture_out(args, opts = {})
output, err, code = Open3.capture3(client_command, *args, binmode: opts[:binmode])
if code != 0
error_msg = "SCM command failed: Non-zero exit code (#{code}) for `#{client_command}`"
logger.error(error_msg)
logger.debug("Error output is #{err}")
raise CommandFailed.new(client_command, stripped_command(args), error_msg)
end
output
end
# Executes the given arguments for +client_command+ on the shell
# and returns stdout, stderr, and the exit code.
#
# If the operation throws an exception or the operation we rethrow a
# +CommandFailed+ with a meaningful error message.
def popen3(args, opts = {}, &block)
logger.debug "Shelling out: `#{stripped_command(args)}`"
Open3.popen3(client_command, *args, opts, &block)
rescue => e
error_msg = "SCM command for `#{client_command}` failed: #{strip_credential(e.message)}"
logger.error(error_msg)
raise CommandFailed.new(client_command, stripped_command(args), error_msg)
end
##
# Returns the full client command and args with stripped credentials
def stripped_command(args)
"#{client_command} #{strip_credential(args.join(' '))}"
end
##
# Replaces argument values for --username/--password in a given command
# with a placeholder
def strip_credential(cmd)
q = Redmine::Platform.mswin? ? '"' : "'"
cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
end
def scm_encode(to, from, str)
return nil if str.nil?
return str if to == from
begin
str.to_s.encode(to, from)
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => err
logger.error("failed to convert from #{from} to #{to}. #{err}")
nil
end
end
end
end
end
end

@ -0,0 +1,282 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'uri'
module OpenProject
module Scm
module Adapters
class Subversion < Base
include LocalClient
def client_command
@client_command ||= scm_config[:subversion_client_command] || 'svn'
end
def client_version
@client_version ||= (svn_binary_version || [])
end
def svn_binary_version
scm_version = scm_version_from_command_line.dup
m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
if m
m[2].scan(%r{\d+}).map(&:to_i)
end
end
def scm_version_from_command_line
capture_out('--version')
end
def initialize(url, root_url = nil, login = nil, password = nil, _path_encoding = nil)
super(url, root_url)
@login = login
@password = password
end
# Get info about the svn repository
def info
cmd = build_svn_cmd(['info', '--xml', target])
xml_capture(cmd, force_encoding: true) do |doc|
Info.new(
root_url: doc.xpath('/info/entry/repository/root').text,
lastrev: extract_revision(doc.at_xpath('/info/entry/commit'))
)
end
end
def entries(path = nil, identifier = nil)
path ||= ''
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
entries = Entries.new
cmd = ['list', '--xml', "#{target(path)}@#{identifier}"]
xml_capture(cmd, force_encoding: true) do |doc|
doc.xpath('/lists/list/entry').each { |list| entries << extract_entry(list, path) }
end
entries.sort_by_name
end
def properties(path, identifier = nil)
# proplist xml output supported in svn 1.5.0 and higher
return nil unless client_version_above?([1, 5, 0])
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
cmd = ['proplist', '--verbose', '--xml', "#{target(path)}@#{identifier}"]
properties = {}
xml_capture(cmd, force_encoding: true) do |doc|
doc.xpath('/properties/target/property').each do |prop|
properties[prop['name']] = prop.text
end
end
properties
end
def revisions(path = nil, identifier_from = nil, identifier_to = nil, options = {})
revisions = Revisions.new
fetch_revision_entries(identifier_from, identifier_to, options, path) do |logentry|
paths = logentry.xpath('paths/path').map { |entry| build_path(entry) }
paths.sort! { |x, y| x[:path] <=> y[:path] }
r = extract_revision(logentry)
r.paths = paths
revisions << r
end
revisions
end
def diff(path, identifier_from, identifier_to = nil, _type = 'inline')
path ||= ''
identifier_from = numeric_identifier(identifier_from)
identifier_to = numeric_identifier(identifier_to, identifier_from - 1)
cmd = ['diff', '-r', "#{identifier_to}:#{identifier_from}",
"#{target(path)}@#{identifier_from}"]
capture_svn(cmd).lines.map(&:chomp)
end
def numeric_identifier(identifier, default = '')
if identifier && identifier.to_i > 0
identifier.to_i
else
default
end
end
def cat(path, identifier = nil)
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
cmd = ['cat', "#{target(path)}@#{identifier}"]
capture_svn(cmd)
end
def annotate(path, identifier = nil)
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
cmd = ['blame', "#{target(path)}@#{identifier}"]
blame = Annotate.new
popen3(cmd) do |io|
io.each_line do |line|
next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
blame.add_line($3.rstrip, Revision.new(identifier: $1.to_i, author: $2.strip))
end
end
blame
end
private
##
# Builds the SVM command arguments around the given parameters
# Appends to the parameter:
# --username, --password if specified for this repository
# --no-auth-cache force re-authentication
# --non-interactive avoid prompts
def build_svn_cmd(args)
if @login.present?
args.push('--username', shell_quote(@login))
args.push('--password', shell_quote(@password)) if @password.present?
end
args.push('--no-auth-cache', '--non-interactive')
end
def xml_capture(cmd, opts = {})
output = capture_svn(cmd, opts)
doc = Nokogiri::XML(output)
# Yield helper methods instead of doc
yield doc
end
def extract_entry(entry, path)
revision = extract_revision(entry.at_xpath('commit'))
kind, size, name = parse_entry(entry)
# Skip directory if there is no commit date (usually that
# means that we don't have read access to it)
return if kind == 'dir' && revision.time.nil?
Entry.new(
name: URI.unescape(name),
path: ((path.empty? ? '' : "#{path}/") + name),
kind: kind,
size: size.nil? ? nil : size.to_i,
lastrev: revision
)
end
def parse_entry(entry)
kind = entry['kind']
size = entry.xpath('size').text
name = entry.xpath('name').text
[kind, size, name]
end
def build_path(entry)
{
action: entry['action'],
path: entry.text,
from_path: entry['copyfrom-path'],
from_revision: entry['copyfrom-rev']
}
end
def extract_revision(commit_node)
# We may be unauthorized to read the commit date
date =
begin
Time.parse(commit_node.xpath('date').text).localtime
rescue ArgumentError
nil
end
Revision.new(
identifier: commit_node['revision'],
time: date,
message: commit_node.xpath('msg').text,
author: commit_node.xpath('author').text
)
end
def fetch_revision_entries(identifier_from, identifier_to, options, path, &block)
path ||= ''
identifier_from = numeric_identifier(identifier_from, 'HEAD')
identifier_to = numeric_identifier(identifier_to, 1)
cmd = ['log', '--xml', '-r', "#{identifier_from}:#{identifier_to}"]
cmd << '--verbose' if options[:with_paths]
cmd << '--limit' << options[:limit].to_s if options[:limit]
cmd << target(path)
xml_capture(cmd, force_encoding: true) do |doc|
doc.xpath('/log/logentry').each &block
end
end
def target(path = '')
base = path.match(/\A\//) ? root_url : url
uri = "#{base}/#{path}"
URI.escape(URI.escape(uri), '[]')
# shell_quote(uri.gsub(/[?<>\*]/, ''))
end
##
# Builds the full git arguments from the parameters
# and return the executed stdout as a string
def capture_svn(args, opt = {})
cmd = build_svn_cmd(args)
output = capture_out(cmd)
if opt[:force_encoding] && output.respond_to?(:force_encoding)
output.force_encoding('UTF-8')
end
output
end
##
# Builds the full git arguments from the parameters
# and calls the given block with in, out, err, thread
# from +Open3#popen3+.
def popen3(args, &block)
cmd = build_svn_cmd(args)
super(cmd) do |_stdin, stdout, _stderr, wait_thr|
block.call(stdout)
process = wait_thr.value
if process.exitstatus != 0
raise CommandFailed, "git exited with non-zero status: #{$?.exitstatus}"
end
end
end
end
end
end
end

@ -27,9 +27,9 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
module Redmine
module OpenProject
module Scm
class Base
class Manager
class << self
def all
@scms

@ -1,365 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'cgi'
module Redmine
module Scm
module Adapters
class CommandFailed < StandardError #:nodoc:
end
class AbstractAdapter #:nodoc:
class << self
def client_command
''
end
# Returns the version of the scm client
# Eg: [1, 5, 0] or [] if unknown
def client_version
[]
end
# Returns the version string of the scm client
# Eg: '1.5.0' or 'Unknown version' if unknown
def client_version_string
v = client_version || 'Unknown version'
v.is_a?(Array) ? v.join('.') : v.to_s
end
# Returns true if the current client version is above
# or equals the given one
# If option is :unknown is set to true, it will return
# true if the client version is unknown
def client_version_above?(v, options = {})
((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
end
def client_available
true
end
def shell_quote(str)
if Redmine::Platform.mswin?
'"' + str.gsub(/"/, '\\"') + '"'
else
"'" + str.gsub(/'/, "'\"'\"'") + "'"
end
end
end
def initialize(url, root_url = nil, login = nil, password = nil,
_path_encoding = 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 supports_cat?
true
end
def supports_annotate?
respond_to?('annotate')
end
def root_url
@root_url
end
def url
@url
end
# get info about the svn repository
def info
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)
parts = path.to_s.split(%r{[\/\\]}).select { |n| !n.blank? }
search_path = parts[0..-2].join('/')
search_name = parts[-1]
if search_path.blank? && search_name.blank?
# Root entry
Entry.new(path: '', kind: 'dir')
else
# Search for the entry in the parent directory
es = entries(search_path, identifier)
es ? es.detect { |e| e.name == search_name } : nil
end
end
# Returns an Entries collection
# or nil if the given path doesn't exist in the repository
def entries(_path = nil, _identifier = nil)
nil
end
def branches
nil
end
def tags
nil
end
def default_branch
nil
end
def properties(_path, _identifier = nil)
nil
end
def revisions(_path = nil, _identifier_from = nil, _identifier_to = nil, _options = {})
nil
end
def diff(_path, _identifier_from, _identifier_to = nil)
nil
end
def cat(_path, _identifier = nil)
nil
end
def with_leading_slash(path)
path ||= ''
(path[0, 1] != '/') ? "/#{path}" : path
end
def with_trailling_slash(path)
path ||= ''
(path[-1, 1] == '/') ? path : "#{path}/"
end
def without_leading_slash(path)
path ||= ''
path.gsub(%r{\A/+}, '')
end
def without_trailling_slash(path)
path ||= ''
(path[-1, 1] == '/') ? path[0..-2] : path
end
def shell_quote(str)
self.class.shell_quote(str)
end
private
def retrieve_root_url
info = self.info
info ? info.root_url : nil
end
def target(path)
path ||= ''
base = path.match(/\A\//) ? root_url : url
shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
end
def logger
self.class.logger
end
def shellout(cmd, &block)
self.class.shellout(cmd, &block)
end
def self.logger
Rails.logger
end
def self.shellout(cmd, &block)
logger.debug "Shelling out: #{strip_credential(cmd)}" if logger && logger.debug?
if Rails.env == 'development'
# Capture stderr when running in dev environment
cmd = "#{cmd} 2>>#{Rails.root}/log/scm.stderr.log"
end
begin
if RUBY_VERSION < '1.9'
mode = 'r+'
else
mode = 'r+:ASCII-8BIT'
end
IO.popen(cmd, mode) do |io|
io.close_write
block.call(io) if block_given?
end
rescue Errno::ENOENT => e
msg = strip_credential(e.message)
# The command failed, log it and re-raise
logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
raise CommandFailed.new(msg)
end
end
# Hides username/password in a given command
def self.strip_credential(cmd)
q = (Redmine::Platform.mswin? ? '"' : "'")
cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
end
def strip_credential(cmd)
self.class.strip_credential(cmd)
end
def scm_encode(to, from, str)
return nil if str.nil?
return str if to == from
begin
str.to_s.encode(to, from)
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => err
logger.error("failed to convert from #{from} to #{to}. #{err}")
nil
end
end
end
class Entries < Array
def sort_by_name
sort {|x, y|
if x.kind == y.kind
x.name.to_s <=> y.name.to_s
else
x.kind <=> y.kind
end
}
end
def revisions
revisions ||= Revisions.new(collect(&: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' == kind
end
def is_dir?
'dir' == 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 :scmid, :name, :author, :time, :message, :paths, :revision, :branch
attr_writer :identifier
def initialize(attributes = {})
self.identifier = attributes[:identifier]
self.scmid = attributes[:scmid]
self.name = attributes[:name] || 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
# Returns the identifier of this revision; see also Changeset model
def identifier
(@identifier || revision).to_s
end
# Returns the readable identifier.
def format_identifier
identifier
end
end
class Annotate
attr_reader :lines, :revisions
def initialize
@lines = []
@revisions = []
end
def add_line(line, revision)
@lines << line
@revisions << revision
end
def content
content = lines.join("\n")
end
def empty?
lines.empty?
end
end
end
end
end

@ -1,371 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'redmine/scm/adapters/abstract_adapter'
module Redmine
module Scm
module Adapters
class GitAdapter < AbstractAdapter
SCM_GIT_REPORT_LAST_COMMIT = true
# Git executable name
GIT_BIN = OpenProject::Configuration['scm_git_command'] || 'git'
# raised if scm command exited with error, e.g. unknown revision.
class ScmCommandAborted < CommandFailed; end
class << self
def client_command
@@bin ||= GIT_BIN
end
def sq_bin
@@sq_bin ||= shell_quote(GIT_BIN)
end
def client_version
@@client_version ||= (scm_command_version || [])
end
def client_available
!client_version.empty?
end
def scm_command_version
scm_version = scm_version_from_command_line.dup
if scm_version.respond_to?(:force_encoding)
scm_version.force_encoding('ASCII-8BIT')
end
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
m[2].scan(%r{\d+}).map(&:to_i)
end
end
def scm_version_from_command_line
shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
end
end
def initialize(url, root_url = nil, login = nil, password = nil, path_encoding = nil)
super
@path_encoding = path_encoding || 'UTF-8'
@flag_report_last_commit = SCM_GIT_REPORT_LAST_COMMIT
end
def info
Info.new(root_url: url, lastrev: lastrev('', nil))
rescue
nil
end
def branches
return @branches if @branches
@branches = []
cmd_args = %w|branch --no-color|
scm_cmd(*cmd_args) do |io|
io.each_line do |line|
@branches << line.match('\s*\*?\s*(.*)$')[1]
end
end
@branches.sort!
rescue ScmCommandAborted
nil
end
def tags
return @tags if @tags
cmd_args = %w|tag|
scm_cmd(*cmd_args) do |io|
@tags = io.readlines.sort!.map(&:strip)
end
rescue ScmCommandAborted
nil
end
def default_branch
bras = branches
return nil if bras.nil?
bras.include?('master') ? 'master' : bras.first
end
def entries(path = nil, identifier = nil)
path ||= ''
p = scm_encode(@path_encoding, 'UTF-8', path)
entries = Entries.new
cmd_args = %w|ls-tree -l|
cmd_args << "HEAD:#{p}" if identifier.nil?
cmd_args << "#{identifier}:#{p}" if identifier
scm_cmd(*cmd_args) do |io|
io.each_line do |line|
e = line.chomp.to_s
if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
type = $1
sha = $2
size = $3
name = $4
if name.respond_to?(:force_encoding)
name.force_encoding(@path_encoding)
end
full_path = p.empty? ? name : "#{p}/#{name}"
n = scm_encode('UTF-8', @path_encoding, name)
full_p = scm_encode('UTF-8', @path_encoding, full_path)
entries << Entry.new(name: n,
path: full_p,
kind: (type == 'tree') ? 'dir' : 'file',
size: (type == 'tree') ? nil : size,
lastrev: @flag_report_last_commit ? lastrev(full_path, identifier) : Revision.new
) unless entries.detect { |entry| entry.name == name }
end
end
end
entries.sort_by_name
rescue ScmCommandAborted
nil
end
def lastrev(path, rev)
return nil if path.nil?
cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
cmd_args << rev if rev
cmd_args << '--' << path unless path.empty?
lines = []
scm_cmd(*cmd_args) { |io| lines = io.readlines }
begin
id = lines[0].split[1]
author = lines[1].match('Author:\s+(.*)$')[1]
time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
Revision.new(
identifier: id,
scmid: id,
author: author,
time: time,
message: nil,
paths: nil
)
rescue NoMethodError => e
logger.error("The revision '#{path}' has a wrong format")
return nil
end
rescue ScmCommandAborted
nil
end
def revisions(path, identifier_from, identifier_to, options = {})
revisions = Revisions.new
cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller|
cmd_args << '--reverse' if options[:reverse]
cmd_args << '--all' if options[:all]
cmd_args << '-n' << "#{options[:limit].to_i}" if options[:limit]
from_to = ''
from_to << "#{identifier_from}.." if identifier_from
from_to << "#{identifier_to}" if identifier_to
cmd_args << from_to if !from_to.empty?
cmd_args << "--since=#{options[:since].strftime('%Y-%m-%d %H:%M:%S')}" if options[:since]
cmd_args << '--' << scm_encode(@path_encoding, 'UTF-8', path) if path && !path.empty?
scm_cmd *cmd_args do |io|
files = []
changeset = {}
parsing_descr = 0 # 0: not parsing desc or files, 1: parsing desc, 2: parsing files
io.each_line do |line|
if line =~ /^commit ([0-9a-f]{40})$/
key = 'commit'
value = $1
if parsing_descr == 1 || parsing_descr == 2
parsing_descr = 0
revision = Revision.new(
identifier: changeset[:commit],
scmid: changeset[:commit],
author: changeset[:author],
time: Time.parse(changeset[:date]),
message: changeset[:description],
paths: files
)
if block_given?
yield revision
else
revisions << revision
end
changeset = {}
files = []
end
changeset[:commit] = $1
elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
key = $1
value = $2
if key == 'Author'
changeset[:author] = value
elsif key == 'CommitDate'
changeset[:date] = value
end
elsif (parsing_descr == 0) && line.chomp.to_s == ''
parsing_descr = 1
changeset[:description] = ''
elsif (parsing_descr == 1 || parsing_descr == 2) \
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
parsing_descr = 2
fileaction = $1
filepath = $2
p = scm_encode('UTF-8', @path_encoding, filepath)
files << { action: fileaction, path: p }
elsif (parsing_descr == 1 || parsing_descr == 2) \
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
parsing_descr = 2
fileaction = $1
filepath = $3
p = scm_encode('UTF-8', @path_encoding, filepath)
files << { action: fileaction, path: p }
elsif (parsing_descr == 1) && line.chomp.to_s == ''
parsing_descr = 2
elsif (parsing_descr == 1)
changeset[:description] << line[4..-1]
end
end
if changeset[:commit]
revision = Revision.new(
identifier: changeset[:commit],
scmid: changeset[:commit],
author: changeset[:author],
time: Time.parse(changeset[:date]),
message: changeset[:description],
paths: files
)
if block_given?
yield revision
else
revisions << revision
end
end
end
revisions
rescue ScmCommandAborted
revisions
end
def diff(path, identifier_from, identifier_to = nil)
path ||= ''
cmd_args = []
if identifier_to
cmd_args << 'diff' << '--no-color' << identifier_to << identifier_from
else
cmd_args << 'show' << '--no-color' << identifier_from
end
cmd_args << '--' << scm_encode(@path_encoding, 'UTF-8', path) unless path.empty?
diff = []
scm_cmd *cmd_args do |io|
io.each_line do |line|
diff << line
end
end
diff
rescue ScmCommandAborted
nil
end
def annotate(path, identifier = nil)
identifier = 'HEAD' if identifier.blank?
cmd_args = %w|blame|
cmd_args << '-p' << identifier << '--' << scm_encode(@path_encoding, 'UTF-8', path)
blame = Annotate.new
content = nil
scm_cmd(*cmd_args) { |io| io.binmode; content = io.read }
# git annotates binary files
if content.respond_to?('is_binary_data?') && content.is_binary_data? # Ruby 1.8.x and <1.9.2
return nil
elsif content.respond_to?(:force_encoding) && (content.dup.force_encoding('UTF-8') != content.dup.force_encoding('BINARY')) # Ruby 1.9.2
# TODO: need to handle edge cases of non-binary content that isn't UTF-8
return nil
end
identifier = ''
# git shows commit author on the first occurrence only
authors_by_commit = {}
content.split("\n").each do |line|
if line =~ /^([0-9a-f]{39,40})\s.*/
identifier = $1
elsif line =~ /^author (.+)/
authors_by_commit[identifier] = $1.strip
elsif line =~ /^\t(.*)/
blame.add_line($1, Revision.new(
identifier: identifier,
author: authors_by_commit[identifier]))
identifier = ''
author = ''
end
end
blame
rescue ScmCommandAborted
nil
end
def cat(path, identifier = nil)
if identifier.nil?
identifier = 'HEAD'
end
cmd_args = %w|show --no-color|
cmd_args << "#{identifier}:#{scm_encode(@path_encoding, 'UTF-8', path)}"
cat = nil
scm_cmd(*cmd_args) do |io|
io.binmode
cat = io.read
end
cat
rescue ScmCommandAborted
nil
end
class Revision < Redmine::Scm::Adapters::Revision
# Returns the readable identifier
def format_identifier
identifier[0, 8]
end
end
def scm_cmd(*args, &block)
repo_path = root_url || url
full_args = [GIT_BIN, '--git-dir', repo_path]
if self.class.client_version_above?([1, 7, 2])
full_args << '-c' << 'core.quotepath=false'
end
full_args += args
ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
if $? && $?.exitstatus != 0
raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
end
ret
end
private :scm_cmd
end
end
end
end

@ -1,293 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# 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.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'redmine/scm/adapters/abstract_adapter'
require 'uri'
module Redmine
module Scm
module Adapters
class SubversionAdapter < AbstractAdapter
# SVN executable name
SVN_BIN = OpenProject::Configuration['scm_subversion_command'] || 'svn'
class << self
def client_command
@@bin ||= SVN_BIN
end
def sq_bin
@@sq_bin ||= shell_quote(SVN_BIN)
end
def client_version
@@client_version ||= (svn_binary_version || [])
end
def client_available
!client_version.empty?
end
def svn_binary_version
scm_version = scm_version_from_command_line.dup
if scm_version.respond_to?(:force_encoding)
scm_version.force_encoding('ASCII-8BIT')
end
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
m[2].scan(%r{\d+}).map(&:to_i)
end
end
def scm_version_from_command_line
shellout("#{sq_bin} --version") { |io| io.read }.to_s
end
end
# Get info about the svn repository
def info
cmd = "#{self.class.sq_bin} info --xml #{target}"
cmd << credentials_string
info = nil
shellout(cmd) do |io|
output = io.read
if output.respond_to?(:force_encoding)
output.force_encoding('UTF-8')
end
begin
doc = ActiveSupport::XmlMini.parse(output)
# root_url = doc.elements["info/entry/repository/root"].text
info = Info.new(root_url: doc['info']['entry']['repository']['root']['__content__'],
lastrev: Revision.new(
identifier: doc['info']['entry']['commit']['revision'],
time: Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
author: (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : '')
)
)
rescue
end
end
return nil if $? && $?.exitstatus != 0
info
rescue CommandFailed
return 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 = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
entries = Entries.new
cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
cmd << credentials_string
shellout(cmd) do |io|
output = io.read
if output.respond_to?(:force_encoding)
output.force_encoding('UTF-8')
end
begin
doc = ActiveSupport::XmlMini.parse(output)
each_xml_element(doc['lists']['list'], 'entry') do |entry|
commit = entry['commit']
commit_date = commit['date']
# Skip directory if there is no commit date (usually that
# means that we don't have read access to it)
next if entry['kind'] == 'dir' && commit_date.nil?
name = entry['name']['__content__']
entries << Entry.new(name: URI.unescape(name),
path: ((path.empty? ? '' : "#{path}/") + name),
kind: entry['kind'],
size: ((s = entry['size']) ? s['__content__'].to_i : nil),
lastrev: Revision.new(
identifier: commit['revision'],
time: Time.parse(commit_date['__content__'].to_s).localtime,
author: ((a = commit['author']) ? a['__content__'] : nil)
)
)
end
rescue => e
logger.error("Error parsing svn output: #{e.message}")
logger.error("Output was:\n #{output}")
end
end
return nil if $? && $?.exitstatus != 0
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
entries.sort_by_name
end
def properties(path, identifier = nil)
# proplist xml output supported in svn 1.5.0 and higher
return nil unless self.class.client_version_above?([1, 5, 0])
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
cmd << credentials_string
properties = {}
shellout(cmd) do |io|
output = io.read
if output.respond_to?(:force_encoding)
output.force_encoding('UTF-8')
end
begin
doc = ActiveSupport::XmlMini.parse(output)
each_xml_element(doc['properties']['target'], 'property') do |property|
properties[property['name']] = property['__content__'].to_s
end
rescue
end
end
return nil if $? && $?.exitstatus != 0
properties
end
def revisions(path = nil, identifier_from = nil, identifier_to = nil, options = {})
path ||= ''
identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : 'HEAD'
identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
revisions = Revisions.new
cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
cmd << credentials_string
cmd << ' --verbose ' if options[:with_paths]
cmd << " --limit #{options[:limit].to_i}" if options[:limit]
cmd << ' ' + target(path)
shellout(cmd) do |io|
output = io.read
if output.respond_to?(:force_encoding)
output.force_encoding('UTF-8')
end
begin
doc = ActiveSupport::XmlMini.parse(output)
each_xml_element(doc['log'], 'logentry') do |logentry|
paths = []
each_xml_element(logentry['paths'], 'path') do |path|
paths << { action: path['action'],
path: path['__content__'],
from_path: path['copyfrom-path'],
from_revision: path['copyfrom-rev']
}
end if logentry['paths'] && logentry['paths']['path']
paths.sort! { |x, y| x[:path] <=> y[:path] }
revisions << Revision.new(identifier: logentry['revision'],
author: (logentry['author'] ? logentry['author']['__content__'] : ''),
time: Time.parse(logentry['date']['__content__'].to_s).localtime,
message: logentry['msg']['__content__'],
paths: paths
)
end
rescue
end
end
return nil if $? && $?.exitstatus != 0
revisions
end
def diff(path, identifier_from, identifier_to = nil, _type = 'inline')
path ||= ''
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
cmd = "#{self.class.sq_bin} diff -r "
cmd << "#{identifier_to}:"
cmd << "#{identifier_from}"
cmd << " #{target(path)}@#{identifier_from}"
cmd << credentials_string
diff = []
shellout(cmd) do |io|
io.each_line do |line|
diff << line
end
end
return nil if $? && $?.exitstatus != 0
diff
end
def cat(path, identifier = nil)
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
cmd << credentials_string
cat = nil
shellout(cmd) do |io|
io.binmode
cat = io.read
end
return nil if $? && $?.exitstatus != 0
cat
end
def annotate(path, identifier = nil)
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : 'HEAD'
cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
cmd << credentials_string
blame = Annotate.new
shellout(cmd) do |io|
io.each_line do |line|
next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
blame.add_line($3.rstrip, Revision.new(identifier: $1.to_i, author: $2.strip))
end
end
return nil if $? && $?.exitstatus != 0
blame
end
private
def credentials_string
str = ''
str << " --username #{shell_quote(@login)}" unless @login.blank?
str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
str << ' --no-auth-cache --non-interactive'
str
end
# Helper that iterates over the child elements of a xml node
# MiniXml returns a hash when a single child is found or an array of hashes for multiple children
def each_xml_element(node, name)
if node && node[name]
if node[name].is_a?(Hash)
yield node[name]
else
node[name].each do |element|
yield element
end
end
end
end
def target(path = '')
base = path.match(/\A\//) ? root_url : url
uri = "#{base}/#{path}"
uri = URI.escape(URI.escape(uri), '[]')
shell_quote(uri.gsub(/[?<>\*]/, ''))
end
end
end
end
end

@ -114,7 +114,7 @@ describe RepositoriesController, 'Subversion', type: :controller do
assert_equal %w(6 3 2), changesets.map(&:revision)
# svn properties displayed with svn >= 1.5 only
if Redmine::Scm::Adapters::SubversionAdapter.client_version_above?([1, 5, 0])
if @repository.scm.client_version_above?([1, 5, 0])
assert_not_nil assigns(:properties)
assert_equal 'native', assigns(:properties)['svn:eol-style']
assert_tag :ul,

@ -29,13 +29,13 @@
require 'legacy_spec_helper'
describe Redmine::Scm::Adapters::FilesystemAdapter, type: :model do
describe OpenProject::Scm::Adapters::FileSystem, type: :model do
let(:fs_repository_path) { Rails.root.to_s.gsub(%r{config\/\.\.}, '') + '/tmp/test/filesystem_repository' }
before do
skip 'Filesystem test repository NOT FOUND. Skipping unit tests !!! See doc/RUNNING_TESTS.' unless File.directory?(fs_repository_path)
@adapter = Redmine::Scm::Adapters::FilesystemAdapter.new(fs_repository_path)
@adapter = OpenProject::Scm::Adapters::FileSystem.new(fs_repository_path)
end
it 'should entries' do

@ -32,7 +32,7 @@
require 'legacy_spec_helper'
describe Redmine::Scm::Adapters::GitAdapter, type: :model do
describe OpenProject::Scm::Adapters::Git, type: :model do
let(:git_repository_path) { Rails.root.to_s.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository' }
FELIX_UTF8 = 'Felix Schäfer'
@ -48,7 +48,7 @@ describe Redmine::Scm::Adapters::GitAdapter, type: :model do
before do
skip 'Git test repository NOT FOUND. Skipping unit tests !!!' unless File.directory?(git_repository_path)
@adapter = Redmine::Scm::Adapters::GitAdapter.new(
@adapter = OpenProject::Scm::Adapters::Git.new(
git_repository_path,
nil,
nil,
@ -127,7 +127,7 @@ describe Redmine::Scm::Adapters::GitAdapter, type: :model do
it 'should annotate' do
annotate = @adapter.annotate('sources/watchers_controller.rb')
assert_kind_of Redmine::Scm::Adapters::Annotate, annotate
assert_kind_of OpenProject::Scm::Adapters::Annotate, annotate
assert_equal 41, annotate.lines.size
assert_equal '# This program is free software; you can redistribute it and/or',
annotate.lines[4].strip
@ -138,7 +138,7 @@ describe Redmine::Scm::Adapters::GitAdapter, type: :model do
it 'should annotate moved file' do
annotate = @adapter.annotate('renamed_test.txt')
assert_kind_of Redmine::Scm::Adapters::Annotate, annotate
assert_kind_of OpenProject::Scm::Adapters::Annotate, annotate
assert_equal 2, annotate.lines.size
end
@ -248,7 +248,7 @@ describe Redmine::Scm::Adapters::GitAdapter, type: :model do
private
def test_scm_version_for(scm_command_version, version)
expect(@adapter.class).to receive(:scm_version_from_command_line).and_return(scm_command_version)
assert_equal version, @adapter.class.scm_command_version
expect(@adapter).to receive(:scm_version_from_command_line).and_return(scm_command_version)
assert_equal version, @adapter.git_binary_version
end
end

@ -29,14 +29,14 @@
require 'legacy_spec_helper'
describe Redmine::Scm::Adapters::SubversionAdapter, type: :model do
describe OpenProject::Scm::Adapters::Subversion, type: :model do
if repository_configured?('subversion')
before do
@adapter = Redmine::Scm::Adapters::SubversionAdapter.new(self.class.subversion_repository_url)
@adapter = OpenProject::Scm::Adapters::Subversion.new(self.class.subversion_repository_url)
end
it 'should client version' do
v = Redmine::Scm::Adapters::SubversionAdapter.client_version
v = @adapter.client_version
assert v.is_a?(Array)
end
@ -53,8 +53,8 @@ describe Redmine::Scm::Adapters::SubversionAdapter, type: :model do
private
def test_scm_version_for(scm_version, version)
expect(@adapter.class).to receive(:scm_version_from_command_line).and_return(scm_version)
assert_equal version, @adapter.class.svn_binary_version
expect(@adapter).to receive(:scm_version_from_command_line).and_return(scm_version)
assert_equal version, @adapter.svn_binary_version
end
else

@ -29,7 +29,7 @@
require 'spec_helper'
describe Redmine::Scm::Base do
describe OpenProject::Scm::Manager do
describe '.configured' do
subject { described_class.configured }
@ -39,12 +39,12 @@ describe Redmine::Scm::Base do
before do
Repository.const_set('TestScm', test_scm_class)
Redmine::Scm::Base.add 'TestScm'
OpenProject::Scm::Manager.add 'TestScm'
end
after do
Repository.send(:remove_const, :TestScm)
Redmine::Scm::Base.delete 'TestScm'
OpenProject::Scm::Manager.delete 'TestScm'
end
context 'scm is configured' do

Loading…
Cancel
Save