Commit f40d4e6685ca749c69bfc480a747a430f6c9825f

Authored by Dmitriy Zaporozhets
2 parents 36efe0f5 ec638048

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

updated fork feature to use gitlab-shell for v5 of gitlab
app/contexts/projects/fork_context.rb 0 → 100644
... ... @@ -0,0 +1,45 @@
  1 +module Projects
  2 + class ForkContext < BaseContext
  3 + include Gitlab::ShellAdapter
  4 +
  5 + def initialize(project, user)
  6 + @from_project, @current_user = project, user
  7 + end
  8 +
  9 + def execute
  10 + project = Project.new
  11 + project.initialize_dup(@from_project)
  12 + project.name = @from_project.name
  13 + project.path = @from_project.path
  14 + project.namespace = current_user.namespace
  15 + project.creator = current_user
  16 +
  17 + # If the project cannot save, we do not want to trigger the project destroy
  18 + # as this can have the side effect of deleting a repo attached to an existing
  19 + # project with the same name and namespace
  20 + if project.valid?
  21 + begin
  22 + Project.transaction do
  23 + #First save the DB entries as they can be rolled back if the repo fork fails
  24 + project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id)
  25 + if project.save
  26 + project.users_projects.create(project_access: UsersProject::MASTER, user: current_user)
  27 + end
  28 + #Now fork the repo
  29 + unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path)
  30 + raise "forking failed in gitlab-shell"
  31 + end
  32 + project.ensure_satellite_exists
  33 + end
  34 + rescue => ex
  35 + project.errors.add(:base, "Fork transaction failed.")
  36 + project.destroy
  37 + end
  38 + else
  39 + project.errors.add(:base, "Invalid fork destination")
  40 + end
  41 + project
  42 +
  43 + end
  44 + end
  45 +end
... ...
app/controllers/projects_controller.rb
... ... @@ -78,4 +78,19 @@ class ProjectsController &lt; ProjectResourceController
78 78 format.html { redirect_to root_path }
79 79 end
80 80 end
  81 +
  82 + def fork
  83 + @project = ::Projects::ForkContext.new(project, current_user).execute
  84 +
  85 + respond_to do |format|
  86 + format.html do
  87 + if @project.saved? && @project.forked?
  88 + redirect_to(@project, notice: 'Project was successfully forked.')
  89 + else
  90 + render action: "new"
  91 + end
  92 + end
  93 + format.js
  94 + end
  95 + end
81 96 end
... ...
app/models/ability.rb
... ... @@ -67,7 +67,9 @@ class Ability
67 67 def project_report_rules
68 68 project_guest_rules + [
69 69 :download_code,
70   - :write_snippet
  70 + :write_snippet,
  71 + :fork_project
  72 +
71 73 ]
72 74 end
73 75  
... ...
app/models/forked_project_link.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +class ForkedProjectLink < ActiveRecord::Base
  2 + attr_accessible :forked_from_project_id, :forked_to_project_id
  3 +
  4 + # Relations
  5 + belongs_to :forked_to_project, class_name: Project
  6 + belongs_to :forked_from_project, class_name: Project
  7 +
  8 +end
... ...
app/models/project.rb
... ... @@ -45,6 +45,8 @@ class Project &lt; ActiveRecord::Base
45 45  
46 46 has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id'
47 47 has_one :gitlab_ci_service, dependent: :destroy
  48 + has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
  49 + has_one :forked_from_project, through: :forked_project_link
48 50  
49 51 has_many :events, dependent: :destroy
50 52 has_many :merge_requests, dependent: :destroy
... ... @@ -402,4 +404,9 @@ class Project &lt; ActiveRecord::Base
402 404 def protected_branch? branch_name
403 405 protected_branches_names.include?(branch_name)
404 406 end
  407 +
  408 + def forked?
  409 + !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
  410 + end
  411 +
405 412 end
... ...
app/observers/project_observer.rb
1 1 class ProjectObserver < BaseObserver
2 2 def after_create(project)
3   - GitlabShellWorker.perform_async(
4   - :add_repository,
5   - project.path_with_namespace
6   - )
  3 + unless project.forked?
  4 + GitlabShellWorker.perform_async(
  5 + :add_repository,
  6 + project.path_with_namespace
  7 + )
7 8  
8   - log_info("#{project.owner.name} created a new project \"#{project.name_with_namespace}\"")
  9 + log_info("#{project.owner.name} created a new project \"#{project.name_with_namespace}\"")
  10 + end
9 11 end
10 12  
11 13 def after_update(project)
... ...
app/views/projects/_clone_panel.html.haml
... ... @@ -5,6 +5,9 @@
5 5 .span3.pull-right
6 6 .pull-right
7 7 - unless @project.empty_repo?
  8 + - if can? current_user, :fork_project, @project
  9 + = link_to fork_project_path(@project), title: "Fork", class: "btn small grouped", method: "POST" do
  10 + Fork
8 11 - if can? current_user, :download_code, @project
9 12 = link_to archive_project_repository_path(@project), class: "btn grouped" do
10 13 %i.icon-download-alt
... ...
config/routes.rb
... ... @@ -167,6 +167,7 @@ Gitlab::Application.routes.draw do
167 167 resources :projects, constraints: { id: /(?:[a-zA-Z.0-9_\-]+\/)?[a-zA-Z.0-9_\-]+/ }, except: [:new, :create, :index], path: "/" do
168 168 member do
169 169 put :transfer
  170 + post :fork
170 171 end
171 172  
172 173 resources :blob, only: [:show], constraints: {id: /.+/}
... ...
db/migrate/20130319214458_create_forked_project_links.rb 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +class CreateForkedProjectLinks < ActiveRecord::Migration
  2 + def change
  3 + create_table :forked_project_links do |t|
  4 + t.integer :forked_to_project_id, null: false
  5 + t.integer :forked_from_project_id, null: false
  6 +
  7 + t.timestamps
  8 + end
  9 + add_index :forked_project_links, :forked_to_project_id, unique: true
  10 + end
  11 +end
... ...
db/schema.rb
... ... @@ -32,6 +32,15 @@ ActiveRecord::Schema.define(:version =&gt; 20130410175022) do
32 32 add_index "events", ["target_id"], :name => "index_events_on_target_id"
33 33 add_index "events", ["target_type"], :name => "index_events_on_target_type"
34 34  
  35 + create_table "forked_project_links", :force => true do |t|
  36 + t.integer "forked_to_project_id", :null => false
  37 + t.integer "forked_from_project_id", :null => false
  38 + t.datetime "created_at", :null => false
  39 + t.datetime "updated_at", :null => false
  40 + end
  41 +
  42 + add_index "forked_project_links", ["forked_to_project_id"], :name => "index_forked_project_links_on_forked_to_project_id", :unique => true
  43 +
35 44 create_table "issues", :force => true do |t|
36 45 t.string "title"
37 46 t.integer "assignee_id"
... ...
features/project/fork_project.feature 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +Feature: Fork Project
  2 + Background:
  3 + Given I sign in as a user
  4 + And I am a member of project "Shop"
  5 + When I visit project "Shop" page
  6 +
  7 + Scenario: User fork a project
  8 + Given I click link "Fork"
  9 + Then I should see the forked project page
  10 +
  11 + Scenario: User already has forked the project
  12 + Given I already have a project named "Shop" in my namespace
  13 + And I click link "Fork"
  14 + Then I should see a "Name has already been taken" warning
... ...
features/steps/project/project_fork.rb 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +class ForkProject < Spinach::FeatureSteps
  2 + include SharedAuthentication
  3 + include SharedPaths
  4 + include SharedProject
  5 +
  6 + step 'I click link "Fork"' do
  7 + Gitlab::Shell.any_instance.stub(:fork_repository).and_return(true)
  8 + click_link "Fork"
  9 + end
  10 +
  11 + step 'I am a member of project "Shop"' do
  12 + @project = Project.find_by_name "Shop"
  13 + @project ||= create(:project_with_code, name: "Shop")
  14 + @project.team << [@user, :reporter]
  15 + end
  16 +
  17 + step 'I should see the forked project page' do
  18 + page.should have_content "Project was successfully forked."
  19 + current_path.should include current_user.namespace.path
  20 + end
  21 +
  22 + step 'I already have a project named "Shop" in my namespace' do
  23 + @my_project = create(:project_with_code, name: "Shop", namespace: current_user.namespace)
  24 + end
  25 +
  26 + step 'I should see a "Name has already been taken" warning' do
  27 + page.should have_content "Name has already been taken"
  28 + end
  29 +
  30 +end
0 31 \ No newline at end of file
... ...
lib/gitlab/backend/shell.rb
... ... @@ -36,6 +36,18 @@ module Gitlab
36 36 system("#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects mv-project #{path}.git #{new_path}.git")
37 37 end
38 38  
  39 + # Fork repository to new namespace
  40 + #
  41 + # path - project path with namespace
  42 + # fork_namespace - namespace for forked project
  43 + #
  44 + # Ex.
  45 + # fork_repository("gitlab/gitlab-ci", "randx")
  46 + #
  47 + def fork_repository(path, fork_namespace)
  48 + system("#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects fork-project #{path}.git #{fork_namespace}")
  49 + end
  50 +
39 51 # Remove repository from file system
40 52 #
41 53 # name - project path with namespace
... ...
spec/contexts/fork_context_spec.rb 0 → 100644
... ... @@ -0,0 +1,57 @@
  1 +require 'spec_helper'
  2 +
  3 +describe Projects::ForkContext do
  4 + describe :fork_by_user do
  5 + before do
  6 + @from_namespace = create(:namespace)
  7 + @from_user = create(:user, namespace: @from_namespace )
  8 + @from_project = create(:project, creator_id: @from_user.id, namespace: @from_namespace)
  9 + @to_namespace = create(:namespace)
  10 + @to_user = create(:user, namespace: @to_namespace)
  11 + end
  12 +
  13 + context 'fork project' do
  14 +
  15 + it "successfully creates project in the user namespace" do
  16 + @to_project = fork_project(@from_project, @to_user)
  17 +
  18 + @to_project.owner.should == @to_user
  19 + @to_project.namespace.should == @to_user.namespace
  20 + end
  21 + end
  22 +
  23 + context 'fork project failure' do
  24 +
  25 + it "fails due to transaction failure" do
  26 + # make the mock gitlab-shell fail
  27 + @to_project = fork_project(@from_project, @to_user, false)
  28 +
  29 + @to_project.errors.should_not be_empty
  30 + @to_project.errors[:base].should include("Fork transaction failed.")
  31 + end
  32 +
  33 + end
  34 +
  35 + context 'project already exists' do
  36 +
  37 + it "should fail due to validation, not transaction failure" do
  38 + @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
  39 + @to_project = fork_project(@from_project, @to_user)
  40 +
  41 + @existing_project.persisted?.should be_true
  42 + @to_project.errors[:base].should include("Invalid fork destination")
  43 + @to_project.errors[:base].should_not include("Fork transaction failed.")
  44 + end
  45 +
  46 + end
  47 + end
  48 +
  49 + def fork_project(from_project, user, fork_success = true)
  50 + context = Projects::ForkContext.new(from_project, user)
  51 + shell = mock("gitlab_shell")
  52 + shell.stub(fork_repository: fork_success)
  53 + context.stub(gitlab_shell: shell)
  54 + context.execute
  55 + end
  56 +
  57 +end
... ...
spec/factories/forked_project_links.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +# Read about factories at https://github.com/thoughtbot/factory_girl
  2 +
  3 +FactoryGirl.define do
  4 + factory :forked_project_link do
  5 + association :forked_to_project, factory: :project
  6 + association :forked_from_project, factory: :project
  7 + end
  8 +end
... ...
spec/lib/gitlab/backend/shell_spec.rb
... ... @@ -12,6 +12,7 @@ describe Gitlab::Shell do
12 12 it { should respond_to :remove_key }
13 13 it { should respond_to :add_repository }
14 14 it { should respond_to :remove_repository }
  15 + it { should respond_to :fork_repository }
15 16  
16 17 it { gitlab_shell.url_to_repo('diaspora').should == Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git" }
17 18 end
... ...
spec/models/forked_project_link_spec.rb 0 → 100644
... ... @@ -0,0 +1,56 @@
  1 +require 'spec_helper'
  2 +
  3 +describe ForkedProjectLink, "add link on fork" do
  4 + let(:project_from) {create(:project)}
  5 + let(:namespace) {create(:namespace)}
  6 + let(:user) {create(:user, namespace: namespace)}
  7 +
  8 + before do
  9 + @project_to = fork_project(project_from, user)
  10 + end
  11 +
  12 + it "project_to should know it is forked" do
  13 + @project_to.forked?.should be_true
  14 + end
  15 +
  16 + it "project should know who it is forked from" do
  17 + @project_to.forked_from_project.should == project_from
  18 + end
  19 +end
  20 +
  21 +describe :forked_from_project do
  22 + let(:forked_project_link) {build(:forked_project_link)}
  23 + let(:project_from) {create(:project)}
  24 + let(:project_to) {create(:project, forked_project_link: forked_project_link)}
  25 +
  26 +
  27 + before :each do
  28 + forked_project_link.forked_from_project = project_from
  29 + forked_project_link.forked_to_project = project_to
  30 + forked_project_link.save!
  31 + end
  32 +
  33 +
  34 + it "project_to should know it is forked" do
  35 + project_to.forked?.should be_true
  36 + end
  37 +
  38 + it "project_from should not be forked" do
  39 + project_from.forked?.should be_false
  40 + end
  41 +
  42 + it "project_to.destroy should destroy fork_link" do
  43 + forked_project_link.should_receive(:destroy)
  44 + project_to.destroy
  45 + end
  46 +
  47 +end
  48 +
  49 +def fork_project(from_project, user)
  50 + context = Projects::ForkContext.new(from_project, user)
  51 + shell = mock("gitlab_shell")
  52 + shell.stub(fork_repository: true)
  53 + context.stub(gitlab_shell: shell)
  54 + context.execute
  55 +end
  56 +
... ...
spec/models/project_spec.rb
... ... @@ -40,6 +40,7 @@ describe Project do
40 40 it { should have_many(:deploy_keys).dependent(:destroy) }
41 41 it { should have_many(:hooks).dependent(:destroy) }
42 42 it { should have_many(:protected_branches).dependent(:destroy) }
  43 + it { should have_one(:forked_project_link).dependent(:destroy) }
43 44 end
44 45  
45 46 describe "Mass assignment" do
... ...
spec/routing/project_routing_spec.rb
... ... @@ -55,6 +55,7 @@ end
55 55  
56 56 # projects POST /projects(.:format) projects#create
57 57 # new_project GET /projects/new(.:format) projects#new
  58 +# fork_project POST /:id/fork(.:format) projects#fork
58 59 # wall_project GET /:id/wall(.:format) projects#wall
59 60 # files_project GET /:id/files(.:format) projects#files
60 61 # edit_project GET /:id/edit(.:format) projects#edit
... ... @@ -70,6 +71,10 @@ describe ProjectsController, &quot;routing&quot; do
70 71 get("/projects/new").should route_to('projects#new')
71 72 end
72 73  
  74 + it "to #fork" do
  75 + post("/gitlabhq/fork").should route_to('projects#fork', id: 'gitlabhq')
  76 + end
  77 +
73 78 it "to #wall" do
74 79 get("/gitlabhq/wall").should route_to('walls#show', project_id: 'gitlabhq')
75 80 end
... ...