diff --git a/app/contexts/projects/fork_context.rb b/app/contexts/projects/fork_context.rb new file mode 100644 index 0000000000000000000000000000000000000000..e206a1cdf8799b4f56d282cfb50c2aa0690cac81 --- /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 8e55aa01cc9657de356bd753db37c9dc935fa511..255baba0ecb4e2291877af33de3373ffaa50f5c4 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 5b49104da8a8fe949c6a3ef30521ca551cbfb8fd..0c5fbc2e5e735eb9bf45a02da87a6c96ffc0a98d 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 0000000000000000000000000000000000000000..c3199ca264e1395659c920e39c3a3a762613651e --- /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 cad8f1666d35bb86e9f66c23c82992a85ee55a83..1d1b7c1134c2a69579212be0f59ae597f7a56e58 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 7d7ecdd319f2d6a6aa4d8d4d5851cce0ade1b366..de9edf41c6dd7503289e5ecc201e4a0b076919f5 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 9a2be4292064ed1b15791f99652e6cd6b8c4feb3..7b90f80985cdc92d4b43c38bdfbcf442079ddfa9 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 18475e032774eb56f9323b08b9c1d19d46cf23c6..8bd6307357a68651d1be93019322bb06210c9a50 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 0000000000000000000000000000000000000000..55aad12093e15477a2e86f2c9e4c16741d2e5dce --- /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 33407e600a46296dfe0f61cf475270722544ecb9..1af91a3b8ee8535a988744e5ed5921db5e7def57 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 0000000000000000000000000000000000000000..dc477ca3bf334b8e47d354b410be8792cf7c3fe0 --- /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 0000000000000000000000000000000000000000..f3335deb279d3023fc2dacb03c7a5812cebabb57 --- /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 bae87977e8d0705c85e1759ad6799d903356f8b9..2c3ea902d939ad445ee33e194b26d75d221054fb 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 0000000000000000000000000000000000000000..285590bdd84bf552d39a0da86d94f51275062edf --- /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 0000000000000000000000000000000000000000..64bcdf0942918a2368ef1f359862af38f47690d5 --- /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 3c04f4bbeb69746503d1893ced3dbc2acfa57674..f00ec0fa401504350dc1a0e155aeabf19e6230c5 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 0000000000000000000000000000000000000000..c362b21f08888f8b3fafcf2bdfb062216eddbf49 --- /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 fedf17b1ba023b488305b1f650100774d47cecd4..b7eb7391072fe6926afd01455a4e6398415ba97a 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 064177f8f2100bcee75642ec9572e90db2352f3b..dd4fb54af69467fd2d39d0f210bd05f6f862215d 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