mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge pull request #3697 from oliverguenther/fix/repository_renaming
Correctly relocate repositories locally and remote after project identifier changes
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user