Merge branch 'master-create-group-projects' into 'master'
Master can create group projects It also includes Project transfer refactoring Fixes #1284
This commit is contained in:
commit
13cbc82aa1
|
@ -46,7 +46,7 @@ class ProjectsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def transfer
|
def transfer
|
||||||
::Projects::TransferService.new(project, current_user, params).execute
|
::Projects::TransferService.new(project, current_user, params[:project]).execute
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
module NamespacesHelper
|
module NamespacesHelper
|
||||||
def namespaces_options(selected = :current_user, scope = :default)
|
def namespaces_options(selected = :current_user, scope = :default)
|
||||||
groups = current_user.owned_groups
|
groups = current_user.owned_groups + current_user.masters_groups
|
||||||
users = [current_user.namespace]
|
users = [current_user.namespace]
|
||||||
|
|
||||||
group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ]
|
group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ]
|
||||||
|
|
|
@ -188,6 +188,13 @@ class Ability
|
||||||
rules << :read_group
|
rules << :read_group
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Only group masters and group owners can create new projects in group
|
||||||
|
if group.has_master?(user) || group.has_owner?(user) || user.admin?
|
||||||
|
rules += [
|
||||||
|
:create_projects,
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
# Only group owner and administrators can manage group
|
# Only group owner and administrators can manage group
|
||||||
if group.has_owner?(user) || user.admin?
|
if group.has_owner?(user) || user.admin?
|
||||||
rules += [
|
rules += [
|
||||||
|
@ -205,6 +212,7 @@ class Ability
|
||||||
# Only namespace owner and administrators can manage it
|
# Only namespace owner and administrators can manage it
|
||||||
if namespace.owner == user || user.admin?
|
if namespace.owner == user || user.admin?
|
||||||
rules += [
|
rules += [
|
||||||
|
:create_projects,
|
||||||
:manage_namespace
|
:manage_namespace
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
@ -60,6 +60,10 @@ class Group < Namespace
|
||||||
owners.include?(user)
|
owners.include?(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_master?(user)
|
||||||
|
members.masters.where(user_id: user).any?
|
||||||
|
end
|
||||||
|
|
||||||
def last_owner?(user)
|
def last_owner?(user)
|
||||||
has_owner?(user) && owners.size == 1
|
has_owner?(user) && owners.size == 1
|
||||||
end
|
end
|
||||||
|
|
|
@ -387,10 +387,6 @@ class Project < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def transfer(new_namespace)
|
|
||||||
ProjectTransferService.new.transfer(self, new_namespace)
|
|
||||||
end
|
|
||||||
|
|
||||||
def execute_hooks(data, hooks_scope = :push_hooks)
|
def execute_hooks(data, hooks_scope = :push_hooks)
|
||||||
hooks.send(hooks_scope).each do |hook|
|
hooks.send(hooks_scope).each do |hook|
|
||||||
hook.async_execute(data)
|
hook.async_execute(data)
|
||||||
|
|
|
@ -90,6 +90,8 @@ class User < ActiveRecord::Base
|
||||||
has_many :users_groups, dependent: :destroy
|
has_many :users_groups, dependent: :destroy
|
||||||
has_many :groups, through: :users_groups
|
has_many :groups, through: :users_groups
|
||||||
has_many :owned_groups, -> { where users_groups: { group_access: UsersGroup::OWNER } }, through: :users_groups, source: :group
|
has_many :owned_groups, -> { where users_groups: { group_access: UsersGroup::OWNER } }, through: :users_groups, source: :group
|
||||||
|
has_many :masters_groups, -> { where users_groups: { group_access: UsersGroup::MASTER } }, through: :users_groups, source: :group
|
||||||
|
|
||||||
# Projects
|
# Projects
|
||||||
has_many :groups_projects, through: :groups, source: :projects
|
has_many :groups_projects, through: :groups, source: :projects
|
||||||
has_many :personal_projects, through: :namespace, source: :projects
|
has_many :personal_projects, through: :namespace, source: :projects
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
# ProjectTransferService class
|
|
||||||
#
|
|
||||||
# Used for transfer project to another namespace
|
|
||||||
#
|
|
||||||
class ProjectTransferService
|
|
||||||
include Gitlab::ShellAdapter
|
|
||||||
|
|
||||||
class TransferError < StandardError; end
|
|
||||||
|
|
||||||
attr_accessor :project
|
|
||||||
|
|
||||||
def transfer(project, new_namespace)
|
|
||||||
Project.transaction do
|
|
||||||
old_path = project.path_with_namespace
|
|
||||||
new_path = File.join(new_namespace.try(:path) || '', project.path)
|
|
||||||
|
|
||||||
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
|
|
||||||
raise TransferError.new("Project with same path in target namespace already exists")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Remove old satellite
|
|
||||||
project.satellite.destroy
|
|
||||||
|
|
||||||
# Apply new namespace id
|
|
||||||
project.namespace = new_namespace
|
|
||||||
project.save!
|
|
||||||
|
|
||||||
# Move main repository
|
|
||||||
unless gitlab_shell.mv_repository(old_path, new_path)
|
|
||||||
raise TransferError.new('Cannot move project')
|
|
||||||
end
|
|
||||||
|
|
||||||
# Move wiki repo also if present
|
|
||||||
gitlab_shell.mv_repository("#{old_path}.wiki", "#{new_path}.wiki")
|
|
||||||
|
|
||||||
# Create a new satellite (reload project from DB)
|
|
||||||
Project.find(project.id).ensure_satellite_exists
|
|
||||||
|
|
||||||
# clear project cached events
|
|
||||||
project.reset_events_cache
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ module Projects
|
||||||
|
|
||||||
def allowed_namespace?(user, namespace_id)
|
def allowed_namespace?(user, namespace_id)
|
||||||
namespace = Namespace.find_by(id: namespace_id)
|
namespace = Namespace.find_by(id: namespace_id)
|
||||||
current_user.can?(:manage_namespace, namespace)
|
current_user.can?(:create_projects, namespace)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,22 +1,71 @@
|
||||||
|
# Projects::TransferService class
|
||||||
|
#
|
||||||
|
# Used for transfer project to another namespace
|
||||||
|
#
|
||||||
|
# Ex.
|
||||||
|
# # Move projects to namespace with ID 17 by user
|
||||||
|
# Projects::TransferService.new(project, user, namespace_id: 17).execute
|
||||||
|
#
|
||||||
module Projects
|
module Projects
|
||||||
class TransferService < BaseService
|
class TransferService < BaseService
|
||||||
def execute(role = :default)
|
include Gitlab::ShellAdapter
|
||||||
namespace_id = params[:project].delete(:namespace_id)
|
class TransferError < StandardError; end
|
||||||
allowed_transfer = can?(current_user, :change_namespace, project) || role == :admin
|
|
||||||
|
|
||||||
if allowed_transfer && namespace_id.present?
|
def execute
|
||||||
if namespace_id.to_i != project.namespace_id
|
namespace_id = params.delete(:namespace_id)
|
||||||
# Transfer to someone namespace
|
namespace = Namespace.find_by(id: namespace_id)
|
||||||
namespace = Namespace.find(namespace_id)
|
|
||||||
project.transfer(namespace)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue ProjectTransferService::TransferError => ex
|
if allowed_transfer?(current_user, project, namespace)
|
||||||
|
transfer(project, namespace)
|
||||||
|
else
|
||||||
|
project.errors.add(:namespace, 'is invalid')
|
||||||
|
false
|
||||||
|
end
|
||||||
|
rescue Projects::TransferService::TransferError => ex
|
||||||
project.reload
|
project.reload
|
||||||
project.errors.add(:namespace_id, ex.message)
|
project.errors.add(:namespace_id, ex.message)
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def transfer(project, new_namespace)
|
||||||
|
Project.transaction do
|
||||||
|
old_path = project.path_with_namespace
|
||||||
|
new_path = File.join(new_namespace.try(:path) || '', project.path)
|
||||||
|
|
||||||
|
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
|
||||||
|
raise TransferError.new("Project with same path in target namespace already exists")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Remove old satellite
|
||||||
|
project.satellite.destroy
|
||||||
|
|
||||||
|
# Apply new namespace id
|
||||||
|
project.namespace = new_namespace
|
||||||
|
project.save!
|
||||||
|
|
||||||
|
# Move main repository
|
||||||
|
unless gitlab_shell.mv_repository(old_path, new_path)
|
||||||
|
raise TransferError.new('Cannot move project')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Move wiki repo also if present
|
||||||
|
gitlab_shell.mv_repository("#{old_path}.wiki", "#{new_path}.wiki")
|
||||||
|
|
||||||
|
# Create a new satellite (reload project from DB)
|
||||||
|
Project.find(project.id).ensure_satellite_exists
|
||||||
|
|
||||||
|
# clear project cached events
|
||||||
|
project.reset_events_cache
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_transfer?(current_user, project, namespace)
|
||||||
|
namespace &&
|
||||||
|
can?(current_user, :change_namespace, project) &&
|
||||||
|
namespace.id != project.namespace_id &&
|
||||||
|
current_user.can?(:create_projects, namespace)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.ui-box
|
.ui-box
|
||||||
.title
|
.title
|
||||||
Projects (#{projects.count})
|
Projects (#{projects.count})
|
||||||
- if can? current_user, :manage_group, @group
|
- if can? current_user, :create_projects, @group
|
||||||
%span.pull-right
|
%span.pull-right
|
||||||
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-new" do
|
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-new" do
|
||||||
%i.icon-plus
|
%i.icon-plus
|
||||||
|
|
|
@ -40,7 +40,7 @@ If a user is a GitLab administrator they receive all permissions.
|
||||||
|------|-----|--------|---------|------|-----|
|
|------|-----|--------|---------|------|-----|
|
||||||
|Browse group|✓|✓|✓|✓|✓|
|
|Browse group|✓|✓|✓|✓|✓|
|
||||||
|Edit group|||||✓|
|
|Edit group|||||✓|
|
||||||
|Create project in group|||||✓|
|
|Create project in group||||✓|✓|
|
||||||
|Manage group members|||||✓|
|
|Manage group members|||||✓|
|
||||||
|Remove group|||||✓|
|
|Remove group|||||✓|
|
||||||
|
|
||||||
|
|
|
@ -87,10 +87,12 @@ module API
|
||||||
# POST /groups/:id/projects/:project_id
|
# POST /groups/:id/projects/:project_id
|
||||||
post ":id/projects/:project_id" do
|
post ":id/projects/:project_id" do
|
||||||
authenticated_as_admin!
|
authenticated_as_admin!
|
||||||
@group = Group.find(params[:id])
|
group = Group.find(params[:id])
|
||||||
project = Project.find(params[:project_id])
|
project = Project.find(params[:project_id])
|
||||||
if project.transfer(@group)
|
result = ::Projects::TransferService.new(project, current_user, namespace_id: group.id).execute
|
||||||
present @group
|
|
||||||
|
if result
|
||||||
|
present group
|
||||||
else
|
else
|
||||||
not_found!
|
not_found!
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
namespace :gitlab do
|
|
||||||
desc "GITLAB | Enable usernames and namespaces for user projects"
|
|
||||||
task enable_namespaces: :environment do
|
|
||||||
warn_user_is_not_gitlab
|
|
||||||
|
|
||||||
migrate_user_namespaces
|
|
||||||
migrate_groups
|
|
||||||
migrate_projects
|
|
||||||
end
|
|
||||||
|
|
||||||
def migrate_user_namespaces
|
|
||||||
puts "\nGenerate usernames for users without one: ".blue
|
|
||||||
User.find_each(batch_size: 500) do |user|
|
|
||||||
if user.namespace
|
|
||||||
print '-'.cyan
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
username = if user.username.present?
|
|
||||||
# if user already has username filled
|
|
||||||
user.username
|
|
||||||
else
|
|
||||||
build_username(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
User.transaction do
|
|
||||||
user.update_attributes!(username: username)
|
|
||||||
print '.'.green
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
print 'F'.red
|
|
||||||
end
|
|
||||||
end
|
|
||||||
puts "\nDone"
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_username(user)
|
|
||||||
username = nil
|
|
||||||
|
|
||||||
# generate username
|
|
||||||
username = user.email.match(/^[^@]*/)[0]
|
|
||||||
username.gsub!("+", ".")
|
|
||||||
|
|
||||||
# return username if no matches
|
|
||||||
return username unless User.find_by(username: username)
|
|
||||||
|
|
||||||
# look for same username
|
|
||||||
(1..10).each do |i|
|
|
||||||
suffixed_username = "#{username}#{i}"
|
|
||||||
|
|
||||||
return suffixed_username unless User.find_by(username: suffixed_username)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def migrate_groups
|
|
||||||
puts "\nCreate directories for groups: ".blue
|
|
||||||
|
|
||||||
Group.find_each(batch_size: 500) do |group|
|
|
||||||
begin
|
|
||||||
if group.dir_exists?
|
|
||||||
print '-'.cyan
|
|
||||||
else
|
|
||||||
if group.ensure_dir_exist
|
|
||||||
print '.'.green
|
|
||||||
else
|
|
||||||
print 'F'.red
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
print 'F'.red
|
|
||||||
end
|
|
||||||
end
|
|
||||||
puts "\nDone"
|
|
||||||
end
|
|
||||||
|
|
||||||
def migrate_projects
|
|
||||||
git_path = Gitlab.config.gitlab_shell.repos_path
|
|
||||||
puts "\nMove projects in groups into respective directories ... ".blue
|
|
||||||
Project.where('namespace_id IS NOT NULL').find_each(batch_size: 500) do |project|
|
|
||||||
next unless project.group
|
|
||||||
|
|
||||||
group = project.group
|
|
||||||
|
|
||||||
print "#{project.name_with_namespace.yellow} ... "
|
|
||||||
|
|
||||||
new_path = File.join(git_path, project.path_with_namespace + '.git')
|
|
||||||
|
|
||||||
if File.exists?(new_path)
|
|
||||||
puts "already at #{new_path}".green
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
old_path = File.join(git_path, project.path + '.git')
|
|
||||||
|
|
||||||
unless File.exists?(old_path)
|
|
||||||
puts "couldn't find it at #{old_path}".red
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
project.transfer(group.path)
|
|
||||||
puts "moved to #{new_path}".green
|
|
||||||
rescue
|
|
||||||
puts "failed moving to #{new_path}".red
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "\nDone"
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -84,7 +84,6 @@ describe Project do
|
||||||
it { should respond_to(:satellite) }
|
it { should respond_to(:satellite) }
|
||||||
it { should respond_to(:update_merge_requests) }
|
it { should respond_to(:update_merge_requests) }
|
||||||
it { should respond_to(:execute_hooks) }
|
it { should respond_to(:execute_hooks) }
|
||||||
it { should respond_to(:transfer) }
|
|
||||||
it { should respond_to(:name_with_namespace) }
|
it { should respond_to(:name_with_namespace) }
|
||||||
it { should respond_to(:owner) }
|
it { should respond_to(:owner) }
|
||||||
it { should respond_to(:path_with_namespace) }
|
it { should respond_to(:path_with_namespace) }
|
||||||
|
|
|
@ -147,7 +147,7 @@ describe API::API, api: true do
|
||||||
describe "POST /groups/:id/projects/:project_id" do
|
describe "POST /groups/:id/projects/:project_id" do
|
||||||
let(:project) { create(:project) }
|
let(:project) { create(:project) }
|
||||||
before(:each) do
|
before(:each) do
|
||||||
project.stub(:transfer).and_return(true)
|
Projects::TransferService.any_instance.stub(execute: true)
|
||||||
Project.stub(:find).and_return(project)
|
Project.stub(:find).and_return(project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -160,8 +160,8 @@ describe API::API, api: true do
|
||||||
|
|
||||||
context "when authenticated as admin" do
|
context "when authenticated as admin" do
|
||||||
it "should transfer project to group" do
|
it "should transfer project to group" do
|
||||||
project.should_receive(:transfer)
|
|
||||||
post api("/groups/#{group1.id}/projects/#{project.id}", admin)
|
post api("/groups/#{group1.id}/projects/#{project.id}", admin)
|
||||||
|
response.status.should == 201
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe ProjectTransferService do
|
describe Projects::TransferService do
|
||||||
before(:each) { enable_observers }
|
before(:each) { enable_observers }
|
||||||
after(:each) {disable_observers}
|
after(:each) {disable_observers}
|
||||||
|
|
||||||
context 'namespace -> namespace' do
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:group) { create(:group) }
|
let(:group) { create(:group) }
|
||||||
|
let(:group2) { create(:group) }
|
||||||
let(:project) { create(:project, namespace: user.namespace) }
|
let(:project) { create(:project, namespace: user.namespace) }
|
||||||
|
|
||||||
|
context 'namespace -> namespace' do
|
||||||
before do
|
before do
|
||||||
@result = service.transfer(project, group)
|
group.add_owner(user)
|
||||||
|
@service = Projects::TransferService.new(project, user, namespace_id: group.id)
|
||||||
|
@service.gitlab_shell.stub(mv_repository: true)
|
||||||
|
@result = @service.execute
|
||||||
end
|
end
|
||||||
|
|
||||||
it { @result.should be_true }
|
it { @result.should be_true }
|
||||||
|
@ -18,16 +22,25 @@ describe ProjectTransferService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'namespace -> no namespace' do
|
context 'namespace -> no namespace' do
|
||||||
let(:user) { create(:user) }
|
before do
|
||||||
let(:project) { create(:project, namespace: user.namespace) }
|
group.add_owner(user)
|
||||||
|
@service = Projects::TransferService.new(project, user, namespace_id: nil)
|
||||||
it { lambda{service.transfer(project, nil)}.should raise_error(ActiveRecord::RecordInvalid) }
|
@service.gitlab_shell.stub(mv_repository: true)
|
||||||
|
@result = @service.execute
|
||||||
end
|
end
|
||||||
|
|
||||||
def service
|
it { @result.should be_false }
|
||||||
service = ProjectTransferService.new
|
it { project.namespace.should == user.namespace }
|
||||||
service.gitlab_shell.stub(mv_repository: true)
|
end
|
||||||
service
|
|
||||||
|
context 'namespace -> not allowed namespace' do
|
||||||
|
before do
|
||||||
|
@service = Projects::TransferService.new(project, user, namespace_id: group2.id)
|
||||||
|
@service.gitlab_shell.stub(mv_repository: true)
|
||||||
|
@result = @service.execute
|
||||||
|
end
|
||||||
|
|
||||||
|
it { @result.should be_false }
|
||||||
|
it { project.namespace.should == user.namespace }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue