Commit a4e98f0e authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'fork-to-group' into 'master'

Fork to group

Fixes #1592

* project can be forked into group
* fork link always lead to fork page where you select namespace where to fork

See merge request !1253
parents d803f210 2388fdd7
...@@ -50,6 +50,7 @@ v 7.4.0 ...@@ -50,6 +50,7 @@ v 7.4.0
- Do not delete tmp/repositories itself during clean-up, only its contents - Do not delete tmp/repositories itself during clean-up, only its contents
- Support for backup uploads to remote storage - Support for backup uploads to remote storage
- Prevent notes polling when there are not notes - Prevent notes polling when there are not notes
- Internal ForkService: Prepare support for fork to a given namespace
- API: Add support for forking a project via the API (Bernhard Kaindl) - API: Add support for forking a project via the API (Bernhard Kaindl)
- API: filter project issues by milestone (Julien Bianchi) - API: filter project issues by milestone (Julien Bianchi)
- Fail harder in the backup script - Fail harder in the backup script
......
...@@ -75,6 +75,8 @@ class Dispatcher ...@@ -75,6 +75,8 @@ class Dispatcher
# Ensure we don't create a particular shortcut handler here. This is # Ensure we don't create a particular shortcut handler here. This is
# already created, where the network graph is created. # already created, where the network graph is created.
shortcut_handler = true shortcut_handler = true
when 'projects:forks:new'
new ProjectFork()
when 'users:show' when 'users:show'
new User() new User()
......
class @ProjectFork
constructor: ->
$('.fork-thumbnail a').on 'click', ->
$('.fork-namespaces').hide()
$('.save-project-loader').show()
...@@ -270,3 +270,28 @@ ul.nav.nav-projects-tabs { ...@@ -270,3 +270,28 @@ ul.nav.nav-projects-tabs {
color: #999; color: #999;
} }
} }
.fork-namespaces {
.thumbnail {
&.fork-exists-thumbnail {
border-color: #EEE;
.caption {
color: #999;
}
}
&.fork-thumbnail {
border-color: #AAA;
&:hover {
background-color: $hover;
}
}
a {
text-decoration: none;
}
}
}
class Projects::ForksController < Projects::ApplicationController
# Authorize
before_filter :authorize_download_code!
before_filter :require_non_empty_project
def new
@namespaces = current_user.manageable_namespaces
@namespaces.delete(@project.namespace)
end
def create
namespace = Namespace.find(params[:namespace_id])
@forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute
if @forked_project.saved? && @forked_project.forked?
redirect_to(@forked_project, notice: 'Project was successfully forked.')
else
@title = 'Fork project'
render :error
end
end
end
...@@ -111,22 +111,6 @@ class ProjectsController < ApplicationController ...@@ -111,22 +111,6 @@ class ProjectsController < ApplicationController
end end
end end
def fork
@forked_project = ::Projects::ForkService.new(project, current_user).execute
respond_to do |format|
format.html do
if @forked_project.saved? && @forked_project.forked?
redirect_to(@forked_project, notice: 'Project was successfully forked.')
else
@title = 'Fork project'
render "fork"
end
end
format.js
end
end
def autocomplete_sources def autocomplete_sources
note_type = params['type'] note_type = params['type']
note_id = params['type_id'] note_id = params['type_id']
......
...@@ -25,4 +25,12 @@ module NamespacesHelper ...@@ -25,4 +25,12 @@ module NamespacesHelper
hidden_field_tag(id, value, class: css_class) hidden_field_tag(id, value, class: css_class)
end end
def namespace_icon(namespace, size = 40)
if namespace.kind_of?(Group)
group_icon(namespace.path)
else
avatar_icon(namespace.owner.email, size)
end
end
end end
...@@ -90,4 +90,8 @@ class Namespace < ActiveRecord::Base ...@@ -90,4 +90,8 @@ class Namespace < ActiveRecord::Base
def kind def kind
type == 'Group' ? 'group' : 'user' type == 'Group' ? 'group' : 'user'
end end
def find_fork_of(project)
projects.joins(:forked_project_link).where('forked_project_links.forked_from_project_id = ?', project.id).first
end
end end
...@@ -551,4 +551,14 @@ class User < ActiveRecord::Base ...@@ -551,4 +551,14 @@ class User < ActiveRecord::Base
UsersStarProject.create!(project: project, user: self) UsersStarProject.create!(project: project, user: self)
end end
end end
def manageable_namespaces
@manageable_namespaces ||=
begin
namespaces = []
namespaces << namespace
namespaces += owned_groups
namespaces += masters_groups
end
end
end end
...@@ -2,11 +2,9 @@ module Projects ...@@ -2,11 +2,9 @@ module Projects
class ForkService < BaseService class ForkService < BaseService
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
def initialize(project, user)
@from_project, @current_user = project, user
end
def execute def execute
@from_project = @project
project_params = { project_params = {
visibility_level: @from_project.visibility_level, visibility_level: @from_project.visibility_level,
description: @from_project.description, description: @from_project.description,
...@@ -15,8 +13,18 @@ module Projects ...@@ -15,8 +13,18 @@ module Projects
project = Project.new(project_params) project = Project.new(project_params)
project.name = @from_project.name project.name = @from_project.name
project.path = @from_project.path project.path = @from_project.path
project.namespace = current_user.namespace project.creator = @current_user
project.creator = current_user
if namespace = @params[:namespace]
project.namespace = namespace
else
project.namespace = @current_user.namespace
end
unless @current_user.can?(:create_projects, project.namespace)
project.errors.add(:namespace, 'insufficient access rights')
return project
end
# If the project cannot save, we do not want to trigger the project destroy # If the project cannot save, we do not want to trigger the project destroy
# as this can have the side effect of deleting a repo attached to an existing # as this can have the side effect of deleting a repo attached to an existing
...@@ -27,7 +35,7 @@ module Projects ...@@ -27,7 +35,7 @@ module Projects
#First save the DB entries as they can be rolled back if the repo fork fails #First save the DB entries as they can be rolled back if the repo fork fails
project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id) project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id)
if project.save if project.save
project.team << [current_user, :master] project.team << [@current_user, :master]
end end
#Now fork the repo #Now fork the repo
unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path) unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path)
...@@ -42,8 +50,8 @@ module Projects ...@@ -42,8 +50,8 @@ module Projects
else else
project.errors.add(:base, "Invalid fork destination") project.errors.add(:base, "Invalid fork destination")
end end
project
project
end end
end end
end end
...@@ -16,11 +16,11 @@ ...@@ -16,11 +16,11 @@
- unless @project.empty_repo? - unless @project.empty_repo?
.fork-buttons .fork-buttons
- if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace
- if current_user.already_forked?(@project) - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to project_path(current_user.fork_of(@project)), title: 'Go to my fork' do = link_to project_path(current_user.fork_of(@project)), title: 'Go to my fork' do
= link_to_toggle_fork = link_to_toggle_fork
- else - else
= link_to fork_project_path(@project), title: "Fork project", method: "POST" do = link_to new_project_fork_path(@project), title: "Fork project" do
= link_to_toggle_fork = link_to_toggle_fork
.star-buttons .star-buttons
......
.alert.alert-danger.alert-block
%h4
%i.fa.fa-code-fork
Fork Error!
%p
You tried to fork
= link_to_project @project
but it failed for the following reason:
- if @forked_project && @forked_project.errors.any?
%p
&ndash;
= @forked_project.errors.full_messages.first
%p
= link_to fork_project_path(@project), title: "Fork", class: "btn", method: "POST" do
%i.fa.fa-code-fork
Try to Fork again
- if @forked_project && !@forked_project.saved?
.alert.alert-danger.alert-block
%h4
%i.fa.fa-code-fork
Fork Error!
%p
You tried to fork
= link_to_project @project
but it failed for the following reason:
- if @forked_project && @forked_project.errors.any?
%p
&ndash;
= @forked_project.errors.full_messages.first
%p
= link_to new_project_fork_path(@project), title: "Fork", class: "btn" do
%i.fa.fa-code-fork
Try to Fork again
%h3.page-title Fork project
%p.lead Select namespace where to fork this project
%hr
.fork-namespaces
- @namespaces.in_groups_of(6, false) do |group|
.row
- group.each do |namespace|
.col-md-2.col-sm-3
- if fork = namespace.find_fork_of(@project)
.thumbnail.fork-exists-thumbnail
= link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do
= image_tag namespace_icon(namespace, 200)
.caption
%h4=namespace.human_name
%p
= namespace.path
- else
.thumbnail.fork-thumbnail
= link_to project_fork_path(@project, namespace_id: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do
= image_tag namespace_icon(namespace, 200)
.caption
%h4=namespace.human_name
%p
= namespace.path
%p.light
Fork is a copy of a project repository.
%br
Forking a repository allows you to do changes without affecting the original project.
.save-project-loader.hide
.center
%h2
%i.fa.fa-spinner.fa-spin
Forking repository
%p Please wait a moment, this page will automatically refresh when ready.
...@@ -181,7 +181,6 @@ Gitlab::Application.routes.draw do ...@@ -181,7 +181,6 @@ Gitlab::Application.routes.draw do
resources :projects, constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/ }, except: [:new, :create, :index], path: "/" do resources :projects, constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/ }, except: [:new, :create, :index], path: "/" do
member do member do
put :transfer put :transfer
post :fork
post :archive post :archive
post :unarchive post :unarchive
post :upload_image post :upload_image
...@@ -232,6 +231,8 @@ Gitlab::Application.routes.draw do ...@@ -232,6 +231,8 @@ Gitlab::Application.routes.draw do
end end
end end
resource :fork, only: [:new, :create]
resource :repository, only: [:show] do resource :repository, only: [:show] do
member do member do
get "archive", constraints: { format: Gitlab::Regex.archive_formats_regex } get "archive", constraints: { format: Gitlab::Regex.archive_formats_regex }
......
...@@ -6,9 +6,11 @@ Feature: Project Fork ...@@ -6,9 +6,11 @@ Feature: Project Fork
Scenario: User fork a project Scenario: User fork a project
Given I click link "Fork" Given I click link "Fork"
When I fork to my namespace
Then I should see the forked project page Then I should see the forked project page
Scenario: User already has forked the project Scenario: User already has forked the project
Given I already have a project named "Shop" in my namespace Given I already have a project named "Shop" in my namespace
And I click link "Fork" And I click link "Fork"
When I fork to my namespace
Then I should see a "Name has already been taken" warning Then I should see a "Name has already been taken" warning
...@@ -25,4 +25,10 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps ...@@ -25,4 +25,10 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'I should see a "Name has already been taken" warning' do step 'I should see a "Name has already been taken" warning' do
page.should have_content "Name has already been taken" page.should have_content "Name has already been taken"
end end
step 'I fork to my namespace' do
within '.fork-namespaces' do
click_link current_user.name
end
end
end end
...@@ -55,7 +55,6 @@ end ...@@ -55,7 +55,6 @@ end
# projects POST /projects(.:format) projects#create # projects POST /projects(.:format) projects#create
# new_project GET /projects/new(.:format) projects#new # new_project GET /projects/new(.:format) projects#new
# fork_project POST /:id/fork(.:format) projects#fork
# files_project GET /:id/files(.:format) projects#files # files_project GET /:id/files(.:format) projects#files
# edit_project GET /:id/edit(.:format) projects#edit # edit_project GET /:id/edit(.:format) projects#edit
# project GET /:id(.:format) projects#show # project GET /:id(.:format) projects#show
...@@ -70,10 +69,6 @@ describe ProjectsController, "routing" do ...@@ -70,10 +69,6 @@ describe ProjectsController, "routing" do
get("/projects/new").should route_to('projects#new') get("/projects/new").should route_to('projects#new')
end end
it "to #fork" do
post("/gitlab/gitlabhq/fork").should route_to('projects#fork', id: 'gitlab/gitlabhq')
end
it "to #edit" do it "to #edit" do
get("/gitlab/gitlabhq/edit").should route_to('projects#edit', id: 'gitlab/gitlabhq') get("/gitlab/gitlabhq/edit").should route_to('projects#edit', id: 'gitlab/gitlabhq')
end end
...@@ -462,3 +457,13 @@ describe Projects::GraphsController, "routing" do ...@@ -462,3 +457,13 @@ describe Projects::GraphsController, "routing" do
get("/gitlab/gitlabhq/graphs/master").should route_to('projects/graphs#show', project_id: 'gitlab/gitlabhq', id: 'master') get("/gitlab/gitlabhq/graphs/master").should route_to('projects/graphs#show', project_id: 'gitlab/gitlabhq', id: 'master')
end end
end end
describe Projects::ForksController, "routing" do
it "to #new" do
get("/gitlab/gitlabhq/fork/new").should route_to("projects/forks#new", project_id: 'gitlab/gitlabhq')
end
it "to #create" do
post("/gitlab/gitlabhq/fork").should route_to("projects/forks#create", project_id: 'gitlab/gitlabhq')
end
end
...@@ -42,10 +42,54 @@ describe Projects::ForkService do ...@@ -42,10 +42,54 @@ describe Projects::ForkService do
end end
end end
def fork_project(from_project, user, fork_success = true) describe :fork_to_namespace do
context = Projects::ForkService.new(from_project, user) before do
shell = double("gitlab_shell") @group_owner = create(:user)
shell.stub(fork_repository: fork_success) @developer = create(:user)
@project = create(:project, creator_id: @group_owner.id,
star_count: 777,
description: 'Wow, such a cool project!')
@group = create(:group)
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
@opts = { namespace: @group }
end
context 'fork project for group' do
it 'group owner successfully forks project into the group' do
to_project = fork_project(@project, @group_owner, true, @opts)
to_project.owner.should == @group
to_project.namespace.should == @group
to_project.name.should == @project.name
to_project.path.should == @project.path
to_project.description.should == @project.description
to_project.star_count.should be_zero
end
end
context 'fork project for group when user not owner' do
it 'group developer should fail to fork project into the group' do
to_project = fork_project(@project, @developer, true, @opts)
to_project.errors[:namespace].should == ['insufficient access rights']
end
end
context 'project already exists in group' do
it 'should fail due to validation, not transaction failure' do
existing_project = create(:project, name: @project.name,
namespace: @group)
to_project = fork_project(@project, @group_owner, true, @opts)
existing_project.persisted?.should be_true
to_project.errors[:base].should == ['Invalid fork destination']
to_project.errors[:name].should == ['has already been taken']
to_project.errors[:path].should == ['has already been taken']
end
end
end
def fork_project(from_project, user, fork_success = true, params = {})
context = Projects::ForkService.new(from_project, user, params)
shell = double('gitlab_shell').stub(fork_repository: fork_success)
context.stub(gitlab_shell: shell) context.stub(gitlab_shell: shell)
context.execute context.execute
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