Relocate repositories locally and remote

This commit introduces relocating a repository as a followup after
a project's identifier has been renamed.
pull/3697/head
Oliver Günther 9 years ago
parent 936f6a165a
commit 589ba8e7b1
  1. 2
      app/controllers/projects_controller.rb
  2. 2
      app/services/scm/create_managed_repository_service.rb
  3. 2
      app/services/scm/delete_managed_repository_service.rb
  4. 60
      app/workers/scm/relocate_repository_job.rb
  5. 49
      app/workers/scm/remote_repository_job.rb
  6. 3
      config/configuration.yml.example
  7. 6
      doc/operation_guides/manual/repository-integration.md
  8. 10
      lib/open_project/scm/manageable_repository.rb
  9. 3
      spec/models/repository/git_spec.rb
  10. 2
      spec/models/repository/subversion_spec.rb
  11. 7
      spec/services/scm/delete_managed_repository_service_spec.rb
  12. 72
      spec/support/scm/relocate_repository.rb

@ -171,7 +171,7 @@ class ProjectsController < ApplicationController
redirect_to action: 'settings', id: @project
end
end
OpenProject::Notifications.send('project_updated', project: @project)
OpenProject::Notifications.send('project_renamed', project: @project)
else
respond_to do |format|
format.html do

@ -46,7 +46,7 @@ class Scm::CreateManagedRepositoryService < Scm::BaseRepositoryService
# creating and deleting repositories, which provides transactional DB access
# as well as filesystem access.
if repository.class.manages_remote?
Scm::CreateRemoteRepositoryJob.new(repository).perform
Scm::CreateRemoteRepositoryJob.new(repository, perform_now: true).perform
else
Scm::CreateLocalRepositoryJob.new(repository).perform
end

@ -38,7 +38,7 @@ class Scm::DeleteManagedRepositoryService < Scm::BaseRepositoryService
return false unless repository.managed?
if repository.class.manages_remote?
Scm::DeleteRemoteRepositoryJob.new(repository).perform
Scm::DeleteRemoteRepositoryJob.new(repository, perform_now: true).perform
true
else
delete_local_repository

@ -0,0 +1,60 @@
#-- 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.
#++
##
# Provides an asynchronous job to relocate a managed repository on the local or remote system
class Scm::RelocateRepositoryJob < Scm::RemoteRepositoryJob
def perform
if repository.class.manages_remote?
relocate_remote
else
relocate_on_disk
end
end
private
##
# POST to the remote managed repository a request to relocate the repository
def relocate_remote
send(repository_request.merge(
action: :relocate,
old_repository: repository.root_url))
end
##
# Tries to relocate the repository on disk.
# As we're performing this in a job and currently have no explicit means
# of error handling in this context, there's not much to do here in case of failure.
def relocate_on_disk
FileUtils.mv repository.root_url, repository.managed_repository_path
repository.update_columns(root_url: repository.managed_repository_path,
url: repository.managed_repository_url)
end
end

@ -37,11 +37,20 @@
class Scm::RemoteRepositoryJob
include OpenProject::BeforeDelayedJob
def initialize(repository)
# TODO currently uses the full repository object,
# as the Job is performed synchronously.
# Change this to serialize the ID once its turned to process asynchronously.
@repository = repository
attr_reader :repository
##
# Initialize the job, optionally saving the whole repository object
# (use only when not serializing the job.)
# As we're using the jobs majorly synchronously for the time being, it saves a db trip.
# When we have error handling for asynchronous tasks, refactor this.
def initialize(repository, perform_now: false)
if perform_now
@repository = repository
else
@repository_id = repository.id
end
end
protected
@ -49,7 +58,7 @@ class Scm::RemoteRepositoryJob
##
# Submits the request to the configured managed remote as JSON.
def send(request)
uri = @repository.class.managed_remote
uri = repository.class.managed_remote
req = ::Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
req.body = request.to_json
@ -60,11 +69,11 @@ class Scm::RemoteRepositoryJob
unless response.is_a? ::Net::HTTPSuccess
info = try_to_parse_response(response.body)
raise OpenProject::Scm::Exceptions::ScmError.new(
I18n.t('repositories.errors.remote_call_failed',
code: response.code,
message: info['message']
)
)
I18n.t('repositories.errors.remote_call_failed',
code: response.code,
message: info['message']
)
)
end
end
@ -72,18 +81,18 @@ class Scm::RemoteRepositoryJob
JSON.parse(body)
rescue JSON::JSONError => e
raise OpenProject::Scm::Exceptions::ScmError.new(
I18n.t('repositories.errors.remote_invalid_response')
)
I18n.t('repositories.errors.remote_invalid_response')
)
end
def repository_request
project = @repository.project
project = repository.project
{
token: @repository.scm.config[:access_token],
identifier: @repository.repository_identifier,
vendor: @repository.vendor,
scm_type: @repository.scm_type,
token: repository.scm.config[:access_token],
identifier: repository.repository_identifier,
vendor: repository.vendor,
scm_type: repository.scm_type,
project: {
id: project.id,
name: project.name,
@ -91,4 +100,8 @@ class Scm::RemoteRepositoryJob
}
}
end
def repository
@repository ||= Repository.find(@repository_id)
end
end

@ -226,10 +226,11 @@ default:
#
# When entering a URL, OpenProject will POST to this resource when repositories are created
# using the following JSON-encoded payload:
# - action: The action to perform (create, delete)
# - action: The action to perform (create, delete, relocate)
# - identifier: The repository identifier name
# - vendor: The SCM vendor of the repository to create
# - project: identifier, name and ID of the associated project
# - old_repository: The known path to the old repository (used during relocate, only)
#
# NOTE: Disabling :managed repositories using disabled_types takes precedence over this setting.
# mode:

@ -37,6 +37,7 @@ The following is an excerpt of the configuration and contains all required infor
# - identifier: The repository identifier name
# - vendor: The SCM vendor of the repository to create
# - project: identifier, name and ID of the associated project
# - old_repository: The known path to the old repository (used during relocate, only)
#
# NOTE: Disabling :managed repositories using disabled_types takes precedence over this setting.
#
@ -96,7 +97,10 @@ Upon creating and deleting repositories in the frontend, OpenProject will POST t
Our main use-case for this feature is to reduce the complexity of permission issues around Subversion mainly in packager, for which a simple Apache wrapper script is used in `extra/Apache/OpenProjectRepoman.pm`.
This functionality is very limited, but may be extended when other use cases arise.
If you're interested in setting up the integration manually outside the context of packager, the following excerpt will help you:
It supports notifications for creating repositories (action `create`), moving repositories (action `relocate`, when a project's identifier has changed), and deleting repositories (action `delete`).
If you're interested in setting up the integration manually outside the context of packager, the following excerpt will help you:
PerlSwitches -I/srv/www/perl-lib -T
PerlLoadModule Apache::OpenProjectRepoman

@ -32,6 +32,16 @@ module OpenProject
module ManageableRepository
def self.included(base)
base.extend(ClassMethods)
##
# Take note when projects are renamed and check for associated managed repositories
OpenProject::Notifications.subscribe('project_renamed') do |payload|
repository = payload[:project].repository
if repository && repository.managed?
Delayed::Job.enqueue ::Scm::RelocateRepositoryJob.new(repository)
end
end
end
module ClassMethods

@ -388,6 +388,9 @@ describe Repository::Git, type: :model do
it_behaves_like 'is a countable repository' do
let(:repository) { instance }
end
end
end
it_behaves_like 'repository can be relocated', :git
end

@ -315,4 +315,6 @@ describe Repository::Subversion, type: :model do
end
end
end
it_behaves_like 'repository can be relocated', :subversion
end

@ -81,13 +81,6 @@ describe Scm::DeleteManagedRepositoryService do
repo
}
before do
allow_any_instance_of(Scm::DeleteLocalRepositoryJob)
.to receive(:repository).and_return(repository)
allow_any_instance_of(Scm::DeleteRemoteRepositoryJob)
.to receive(:repository).and_return(repository)
end
it 'deletes the repository' do
expect(File.directory?(repository.root_url)).to be true
expect(service.call).to be true

@ -0,0 +1,72 @@
shared_examples_for 'repository can be relocated' do |vendor|
let(:job) { ::Scm::RelocateRepositoryJob.new repository }
let(:project) { FactoryGirl.build :project }
let(:repository) {
repo = FactoryGirl.build("repository_#{vendor}".to_sym,
project: project,
scm_type: :managed)
repo.configure(:managed, nil)
repo.save!
repo
}
before do
allow(::Scm::RelocateRepositoryJob).to receive(:new).and_return(job)
allow(Repository).to receive(:find).and_return(repository)
end
context 'with managed local config' do
include_context 'with tmpdir'
let(:config) { { manages: File.join(tmpdir, 'myrepos') } }
it 'relocates when project identifier is updated' do
current_path = repository.root_url
expect(repository.root_url).to eq(repository.managed_repository_path)
expect(Dir.exists?(repository.managed_repository_path)).to be true
# Rename the project
project.update_attributes!(identifier: 'somenewidentifier')
repository.reload
job.perform
# Confirm that all paths are updated
expect(current_path).not_to eq(repository.managed_repository_path)
expect(current_path).not_to eq(repository.root_url)
expect(repository.url).to eq(repository.managed_repository_url)
expect(Dir.exists?(repository.managed_repository_path)).to be true
end
end
context 'with managed remote config', webmock: true do
let(:url) { 'http://myreposerver.example.com/api/' }
let(:config) { { manages: url } }
let(:repository) {
stub_request(:post, url).to_return(status: 200)
FactoryGirl.create("repository_#{vendor}".to_sym,
project: project,
scm_type: :managed)
}
before do
stub_request(:post, url).to_return(status: 200)
end
it 'sends a relocation request when project identifier is updated' do
current_path = repository.root_url
# Rename the project
project.identifier = 'somenewidentifier'
job.perform
expect(WebMock)
.to have_requested(:post, url)
.with(body: hash_including(old_repository: current_path,
action: 'relocate'))
end
end
end
Loading…
Cancel
Save