Commit f40d4e66 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge pull request #3597 from amacarthur/fork-pull-request

updated fork feature to use gitlab-shell for v5 of gitlab
parents 36efe0f5 ec638048
module Projects
class ForkContext < BaseContext
include Gitlab::ShellAdapter
def initialize(project, user)
@from_project, @current_user = project, user
end
def execute
project = Project.new
project.initialize_dup(@from_project)
project.name = @from_project.name
project.path = @from_project.path
project.namespace = current_user.namespace
project.creator = current_user
# 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
# project with the same name and namespace
if project.valid?
begin
Project.transaction do
#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)
if project.save
project.users_projects.create(project_access: UsersProject::MASTER, user: current_user)
end
#Now fork the repo
unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path)
raise "forking failed in gitlab-shell"
end
project.ensure_satellite_exists
end
rescue => ex
project.errors.add(:base, "Fork transaction failed.")
project.destroy
end
else
project.errors.add(:base, "Invalid fork destination")
end
project
end
end
end
...@@ -78,4 +78,19 @@ class ProjectsController < ProjectResourceController ...@@ -78,4 +78,19 @@ class ProjectsController < ProjectResourceController
format.html { redirect_to root_path } format.html { redirect_to root_path }
end end
end end
def fork
@project = ::Projects::ForkContext.new(project, current_user).execute
respond_to do |format|
format.html do
if @project.saved? && @project.forked?
redirect_to(@project, notice: 'Project was successfully forked.')
else
render action: "new"
end
end
format.js
end
end
end end
...@@ -67,7 +67,9 @@ class Ability ...@@ -67,7 +67,9 @@ class Ability
def project_report_rules def project_report_rules
project_guest_rules + [ project_guest_rules + [
:download_code, :download_code,
:write_snippet :write_snippet,
:fork_project
] ]
end end
......
class ForkedProjectLink < ActiveRecord::Base
attr_accessible :forked_from_project_id, :forked_to_project_id
# Relations
belongs_to :forked_to_project, class_name: Project
belongs_to :forked_from_project, class_name: Project
end
...@@ -45,6 +45,8 @@ class Project < ActiveRecord::Base ...@@ -45,6 +45,8 @@ class Project < ActiveRecord::Base
has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id' has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id'
has_one :gitlab_ci_service, dependent: :destroy has_one :gitlab_ci_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
has_many :events, dependent: :destroy has_many :events, dependent: :destroy
has_many :merge_requests, dependent: :destroy has_many :merge_requests, dependent: :destroy
...@@ -402,4 +404,9 @@ class Project < ActiveRecord::Base ...@@ -402,4 +404,9 @@ class Project < ActiveRecord::Base
def protected_branch? branch_name def protected_branch? branch_name
protected_branches_names.include?(branch_name) protected_branches_names.include?(branch_name)
end end
def forked?
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
end end
class ProjectObserver < BaseObserver class ProjectObserver < BaseObserver
def after_create(project) def after_create(project)
unless project.forked?
GitlabShellWorker.perform_async( GitlabShellWorker.perform_async(
:add_repository, :add_repository,
project.path_with_namespace project.path_with_namespace
...@@ -7,6 +8,7 @@ class ProjectObserver < BaseObserver ...@@ -7,6 +8,7 @@ class ProjectObserver < BaseObserver
log_info("#{project.owner.name} created a new project \"#{project.name_with_namespace}\"") log_info("#{project.owner.name} created a new project \"#{project.name_with_namespace}\"")
end end
end
def after_update(project) def after_update(project)
project.send_move_instructions if project.namespace_id_changed? project.send_move_instructions if project.namespace_id_changed?
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
.span3.pull-right .span3.pull-right
.pull-right .pull-right
- unless @project.empty_repo? - unless @project.empty_repo?
- if can? current_user, :fork_project, @project
= link_to fork_project_path(@project), title: "Fork", class: "btn small grouped", method: "POST" do
Fork
- if can? current_user, :download_code, @project - if can? current_user, :download_code, @project
= link_to archive_project_repository_path(@project), class: "btn grouped" do = link_to archive_project_repository_path(@project), class: "btn grouped" do
%i.icon-download-alt %i.icon-download-alt
......
...@@ -167,6 +167,7 @@ Gitlab::Application.routes.draw do ...@@ -167,6 +167,7 @@ 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
end end
resources :blob, only: [:show], constraints: {id: /.+/} resources :blob, only: [:show], constraints: {id: /.+/}
......
class CreateForkedProjectLinks < ActiveRecord::Migration
def change
create_table :forked_project_links do |t|
t.integer :forked_to_project_id, null: false
t.integer :forked_from_project_id, null: false
t.timestamps
end
add_index :forked_project_links, :forked_to_project_id, unique: true
end
end
...@@ -32,6 +32,15 @@ ActiveRecord::Schema.define(:version => 20130410175022) do ...@@ -32,6 +32,15 @@ ActiveRecord::Schema.define(:version => 20130410175022) do
add_index "events", ["target_id"], :name => "index_events_on_target_id" add_index "events", ["target_id"], :name => "index_events_on_target_id"
add_index "events", ["target_type"], :name => "index_events_on_target_type" add_index "events", ["target_type"], :name => "index_events_on_target_type"
create_table "forked_project_links", :force => true do |t|
t.integer "forked_to_project_id", :null => false
t.integer "forked_from_project_id", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
add_index "forked_project_links", ["forked_to_project_id"], :name => "index_forked_project_links_on_forked_to_project_id", :unique => true
create_table "issues", :force => true do |t| create_table "issues", :force => true do |t|
t.string "title" t.string "title"
t.integer "assignee_id" t.integer "assignee_id"
......
Feature: Fork Project
Background:
Given I sign in as a user
And I am a member of project "Shop"
When I visit project "Shop" page
Scenario: User fork a project
Given I click link "Fork"
Then I should see the forked project page
Scenario: User already has forked the project
Given I already have a project named "Shop" in my namespace
And I click link "Fork"
Then I should see a "Name has already been taken" warning
class ForkProject < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
step 'I click link "Fork"' do
Gitlab::Shell.any_instance.stub(:fork_repository).and_return(true)
click_link "Fork"
end
step 'I am a member of project "Shop"' do
@project = Project.find_by_name "Shop"
@project ||= create(:project_with_code, name: "Shop")
@project.team << [@user, :reporter]
end
step 'I should see the forked project page' do
page.should have_content "Project was successfully forked."
current_path.should include current_user.namespace.path
end
step 'I already have a project named "Shop" in my namespace' do
@my_project = create(:project_with_code, name: "Shop", namespace: current_user.namespace)
end
step 'I should see a "Name has already been taken" warning' do
page.should have_content "Name has already been taken"
end
end
\ No newline at end of file
...@@ -36,6 +36,18 @@ module Gitlab ...@@ -36,6 +36,18 @@ module Gitlab
system("#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects mv-project #{path}.git #{new_path}.git") system("#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects mv-project #{path}.git #{new_path}.git")
end end
# Fork repository to new namespace
#
# path - project path with namespace
# fork_namespace - namespace for forked project
#
# Ex.
# fork_repository("gitlab/gitlab-ci", "randx")
#
def fork_repository(path, fork_namespace)
system("#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects fork-project #{path}.git #{fork_namespace}")
end
# Remove repository from file system # Remove repository from file system
# #
# name - project path with namespace # name - project path with namespace
......
require 'spec_helper'
describe Projects::ForkContext do
describe :fork_by_user do
before do
@from_namespace = create(:namespace)
@from_user = create(:user, namespace: @from_namespace )
@from_project = create(:project, creator_id: @from_user.id, namespace: @from_namespace)
@to_namespace = create(:namespace)
@to_user = create(:user, namespace: @to_namespace)
end
context 'fork project' do
it "successfully creates project in the user namespace" do
@to_project = fork_project(@from_project, @to_user)
@to_project.owner.should == @to_user
@to_project.namespace.should == @to_user.namespace
end
end
context 'fork project failure' do
it "fails due to transaction failure" do
# make the mock gitlab-shell fail
@to_project = fork_project(@from_project, @to_user, false)
@to_project.errors.should_not be_empty
@to_project.errors[:base].should include("Fork transaction failed.")
end
end
context 'project already exists' do
it "should fail due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user)
@existing_project.persisted?.should be_true
@to_project.errors[:base].should include("Invalid fork destination")
@to_project.errors[:base].should_not include("Fork transaction failed.")
end
end
end
def fork_project(from_project, user, fork_success = true)
context = Projects::ForkContext.new(from_project, user)
shell = mock("gitlab_shell")
shell.stub(fork_repository: fork_success)
context.stub(gitlab_shell: shell)
context.execute
end
end
# Read about factories at https://github.com/thoughtbot/factory_girl
FactoryGirl.define do
factory :forked_project_link do
association :forked_to_project, factory: :project
association :forked_from_project, factory: :project
end
end
...@@ -12,6 +12,7 @@ describe Gitlab::Shell do ...@@ -12,6 +12,7 @@ describe Gitlab::Shell do
it { should respond_to :remove_key } it { should respond_to :remove_key }
it { should respond_to :add_repository } it { should respond_to :add_repository }
it { should respond_to :remove_repository } it { should respond_to :remove_repository }
it { should respond_to :fork_repository }
it { gitlab_shell.url_to_repo('diaspora').should == Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git" } it { gitlab_shell.url_to_repo('diaspora').should == Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git" }
end end
require 'spec_helper'
describe ForkedProjectLink, "add link on fork" do
let(:project_from) {create(:project)}
let(:namespace) {create(:namespace)}
let(:user) {create(:user, namespace: namespace)}
before do
@project_to = fork_project(project_from, user)
end
it "project_to should know it is forked" do
@project_to.forked?.should be_true
end
it "project should know who it is forked from" do
@project_to.forked_from_project.should == project_from
end
end
describe :forked_from_project do
let(:forked_project_link) {build(:forked_project_link)}
let(:project_from) {create(:project)}
let(:project_to) {create(:project, forked_project_link: forked_project_link)}
before :each do
forked_project_link.forked_from_project = project_from
forked_project_link.forked_to_project = project_to
forked_project_link.save!
end
it "project_to should know it is forked" do
project_to.forked?.should be_true
end
it "project_from should not be forked" do
project_from.forked?.should be_false
end
it "project_to.destroy should destroy fork_link" do
forked_project_link.should_receive(:destroy)
project_to.destroy
end
end
def fork_project(from_project, user)
context = Projects::ForkContext.new(from_project, user)
shell = mock("gitlab_shell")
shell.stub(fork_repository: true)
context.stub(gitlab_shell: shell)
context.execute
end
...@@ -40,6 +40,7 @@ describe Project do ...@@ -40,6 +40,7 @@ describe Project do
it { should have_many(:deploy_keys).dependent(:destroy) } it { should have_many(:deploy_keys).dependent(:destroy) }
it { should have_many(:hooks).dependent(:destroy) } it { should have_many(:hooks).dependent(:destroy) }
it { should have_many(:protected_branches).dependent(:destroy) } it { should have_many(:protected_branches).dependent(:destroy) }
it { should have_one(:forked_project_link).dependent(:destroy) }
end end
describe "Mass assignment" do describe "Mass assignment" do
......
...@@ -55,6 +55,7 @@ end ...@@ -55,6 +55,7 @@ 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
# wall_project GET /:id/wall(.:format) projects#wall # wall_project GET /:id/wall(.:format) projects#wall
# 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
...@@ -70,6 +71,10 @@ describe ProjectsController, "routing" do ...@@ -70,6 +71,10 @@ 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("/gitlabhq/fork").should route_to('projects#fork', id: 'gitlabhq')
end
it "to #wall" do it "to #wall" do
get("/gitlabhq/wall").should route_to('walls#show', project_id: 'gitlabhq') get("/gitlabhq/wall").should route_to('walls#show', project_id: 'gitlabhq')
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