summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/contexts/projects/fork_context.rb37
-rw-r--r--app/controllers/projects_controller.rb15
-rw-r--r--app/models/ability.rb3
-rw-r--r--app/models/forked_project_link.rb8
-rw-r--r--app/models/project.rb7
-rw-r--r--app/observers/project_observer.rb12
-rw-r--r--app/views/projects/_clone_panel.html.haml3
-rw-r--r--config/routes.rb1
-rw-r--r--db/migrate/20130319214458_create_forked_project_links.rb11
-rw-r--r--db/schema.rb9
-rw-r--r--features/project/fork_project.feature14
-rw-r--r--features/steps/project/project_fork.rb30
-rw-r--r--lib/gitlab/backend/shell.rb12
-rw-r--r--spec/contexts/fork_context_spec.rb41
-rw-r--r--spec/factories/forked_project_links.rb8
-rw-r--r--spec/lib/gitlab/backend/shell_spec.rb1
-rw-r--r--spec/models/forked_project_link_spec.rb56
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/routing/project_routing_spec.rb5
19 files changed, 268 insertions, 6 deletions
diff --git a/app/contexts/projects/fork_context.rb b/app/contexts/projects/fork_context.rb
new file mode 100644
index 00000000000..e206a1cdf87
--- /dev/null
+++ b/app/contexts/projects/fork_context.rb
@@ -0,0 +1,37 @@
+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.transaction do
+ #First save the DB entries as they can be rolled back if the repo fork fails
+ project.creator = current_user
+ 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
+ project
+ rescue => ex
+ project.errors.add(:base, "Can't fork project. Please try again later")
+ project.destroy
+ end
+
+ end
+end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 8e55aa01cc9..255baba0ecb 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -78,4 +78,19 @@ class ProjectsController < ProjectResourceController
format.html { redirect_to root_path }
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
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 5b49104da8a..0c5fbc2e5e7 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -60,7 +60,8 @@ class Ability
:read_note,
:write_project,
:write_issue,
- :write_note
+ :write_note,
+ :fork_project
]
end
diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb
new file mode 100644
index 00000000000..c3199ca264e
--- /dev/null
+++ b/app/models/forked_project_link.rb
@@ -0,0 +1,8 @@
+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
diff --git a/app/models/project.rb b/app/models/project.rb
index cad8f1666d3..1d1b7c1134c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -43,6 +43,8 @@ class Project < ActiveRecord::Base
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 :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 :merge_requests, dependent: :destroy
@@ -400,4 +402,9 @@ class Project < ActiveRecord::Base
def protected_branch? branch_name
protected_branches_names.include?(branch_name)
end
+
+ def forked?
+ !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
+ end
+
end
diff --git a/app/observers/project_observer.rb b/app/observers/project_observer.rb
index 7d7ecdd319f..de9edf41c6d 100644
--- a/app/observers/project_observer.rb
+++ b/app/observers/project_observer.rb
@@ -1,11 +1,13 @@
class ProjectObserver < BaseObserver
def after_create(project)
- GitlabShellWorker.perform_async(
- :add_repository,
- project.path_with_namespace
- )
+ unless project.forked?
+ GitlabShellWorker.perform_async(
+ :add_repository,
+ project.path_with_namespace
+ )
- 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
def after_update(project)
diff --git a/app/views/projects/_clone_panel.html.haml b/app/views/projects/_clone_panel.html.haml
index 9a2be429206..7b90f80985c 100644
--- a/app/views/projects/_clone_panel.html.haml
+++ b/app/views/projects/_clone_panel.html.haml
@@ -5,6 +5,9 @@
.span4.pull-right
.pull-right
- 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
= link_to archive_project_repository_path(@project), class: "btn-small btn grouped" do
%i.icon-download-alt
diff --git a/config/routes.rb b/config/routes.rb
index 18475e03277..8bd6307357a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
member do
put :transfer
+ post :fork
end
resources :blob, only: [:show], constraints: {id: /.+/}
diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb
new file mode 100644
index 00000000000..55aad12093e
--- /dev/null
+++ b/db/migrate/20130319214458_create_forked_project_links.rb
@@ -0,0 +1,11 @@
+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
diff --git a/db/schema.rb b/db/schema.rb
index 33407e600a4..1af91a3b8ee 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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_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|
t.string "title"
t.integer "assignee_id"
diff --git a/features/project/fork_project.feature b/features/project/fork_project.feature
new file mode 100644
index 00000000000..dc477ca3bf3
--- /dev/null
+++ b/features/project/fork_project.feature
@@ -0,0 +1,14 @@
+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
diff --git a/features/steps/project/project_fork.rb b/features/steps/project/project_fork.rb
new file mode 100644
index 00000000000..f3335deb279
--- /dev/null
+++ b/features/steps/project/project_fork.rb
@@ -0,0 +1,30 @@
+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
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index bae87977e8d..2c3ea902d93 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -36,6 +36,18 @@ module Gitlab
system("#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects mv-project #{path}.git #{new_path}.git")
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
#
# name - project path with namespace
diff --git a/spec/contexts/fork_context_spec.rb b/spec/contexts/fork_context_spec.rb
new file mode 100644
index 00000000000..285590bdd84
--- /dev/null
+++ b/spec/contexts/fork_context_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Projects::ForkContext do
+ describe :fork_by_user do
+ before do
+ @from_user = create :user
+ @from_project = create(:project, creator_id: @from_user.id)
+ @to_user = create :user
+ end
+
+ context 'fork project' do
+ before do
+ @to_project = fork_project(@from_project, @to_user)
+ end
+
+ it { @to_project.owner.should == @to_user }
+ it { @to_project.namespace.should == @to_user.namespace }
+ end
+
+ context 'fork project failure' do
+ before do
+ #corrupt the project so the attempt to fork will fail
+ @from_project = create(:project, path: "empty")
+ @to_project = fork_project(@from_project, @to_user, false)
+ end
+
+ it {@to_project.errors.should_not be_empty}
+ it {@to_project.errors[:base].should include("Can't fork project. Please try again later") }
+
+ 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
diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb
new file mode 100644
index 00000000000..64bcdf09429
--- /dev/null
+++ b/spec/factories/forked_project_links.rb
@@ -0,0 +1,8 @@
+# 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
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index 3c04f4bbeb6..f00ec0fa401 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -12,6 +12,7 @@ describe Gitlab::Shell do
it { should respond_to :remove_key }
it { should respond_to :add_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" }
end
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
new file mode 100644
index 00000000000..c362b21f088
--- /dev/null
+++ b/spec/models/forked_project_link_spec.rb
@@ -0,0 +1,56 @@
+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
+
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index fedf17b1ba0..b7eb7391072 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -40,6 +40,7 @@ describe Project do
it { should have_many(:deploy_keys).dependent(:destroy) }
it { should have_many(:hooks).dependent(:destroy) }
it { should have_many(:protected_branches).dependent(:destroy) }
+ it { should have_one(:forked_project_link).dependent(:destroy) }
end
describe "Mass assignment" do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 064177f8f21..dd4fb54af69 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -55,6 +55,7 @@ end
# projects POST /projects(.:format) projects#create
# new_project GET /projects/new(.:format) projects#new
+# fork_project POST /:id/fork(.:format) projects#fork
# wall_project GET /:id/wall(.:format) projects#wall
# files_project GET /:id/files(.:format) projects#files
# edit_project GET /:id/edit(.:format) projects#edit
@@ -70,6 +71,10 @@ describe ProjectsController, "routing" do
get("/projects/new").should route_to('projects#new')
end
+ it "to #fork" do
+ post("/gitlabhq/fork").should route_to('projects#fork', id: 'gitlabhq')
+ end
+
it "to #wall" do
get("/gitlabhq/wall").should route_to('walls#show', project_id: 'gitlabhq')
end