Commit 13cbc82a authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master-create-group-projects' into 'master'

Master can create group projects

It also includes Project transfer refactoring

Fixes #1284
parents 64b1956b cea0fc1d
...@@ -46,7 +46,7 @@ class ProjectsController < ApplicationController ...@@ -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
......
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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
# 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 ...@@ -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
# 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 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
.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. ...@@ -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 ...@@ -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
......
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 ...@@ -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 ...@@ -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 ...@@ -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
......
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 ...@@ -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)
@service.gitlab_shell.stub(mv_repository: true)
@result = @service.execute
end
it { lambda{service.transfer(project, nil)}.should raise_error(ActiveRecord::RecordInvalid) } it { @result.should be_false }
it { project.namespace.should == user.namespace }
end end
def service context 'namespace -> not allowed namespace' do
service = ProjectTransferService.new before do
service.gitlab_shell.stub(mv_repository: true) @service = Projects::TransferService.new(project, user, namespace_id: group2.id)
service @service.gitlab_shell.stub(mv_repository: true)
@result = @service.execute
end end
end
it { @result.should be_false }
it { project.namespace.should == user.namespace }
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment