Commit d3280072 authored by Bob Van Landuyt's avatar Bob Van Landuyt
Browse files

Create a fork network when forking a project

When no fork network exists for the source projects, we create a new
one with the correct source
parent 31b39063e9be
class ForkNetwork < ActiveRecord::Base
belongs_to :root_project
belongs_to :root_project, class_name: 'Project'
has_many :fork_network_members
has_many :projects, through: :fork_network_members
after_create :add_root_as_member, if: :root_project
def add_root_as_member
projects << root_project
end
end
class ForkNetworkMember < ActiveRecord::Base
belongs_to :fork_network
belongs_to :project
belongs_to :forked_from_project, class_name: 'Project'
validates :fork_network, :project, presence: true
end
......@@ -118,11 +118,20 @@ class Project < ActiveRecord::Base
has_one :mock_monitoring_service
has_one :microsoft_teams_service
# TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id"
has_many :forks, through: :forked_project_links, source: :forked_to_project
# TODO: replace these relations with the fork network versions
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
inverse_of: :root_project,
class_name: 'ForkNetwork'
has_one :fork_network_member
has_one :fork_network, through: :fork_network_member
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
......@@ -1119,8 +1128,8 @@ def change_head(branch)
end
end
def forked_from?(project)
forked? && project == forked_from_project
def in_fork_network_of?(project)
forked? && project.fork_network == fork_network
end
def origin_merge_requests
......
......@@ -23,11 +23,31 @@ def execute
refresh_forks_count
link_fork_network(new_project)
new_project
end
private
def fork_network
if @project.fork_network
@project.fork_network
elsif forked_from_project = @project.forked_from_project
# TODO: remove this case when all background migrations have completed
# this only happens when a project had a `forked_project_link` that was
# not migrated to the `fork_network` relation
forked_from_project.fork_network || forked_from_project.create_root_of_fork_network
else
@project.create_root_of_fork_network
end
end
def link_fork_network(new_project)
fork_network.fork_network_members.create(project: new_project,
forked_from_project: @project)
end
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
end
......
class CreateForkNetworkMembers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :fork_network_members do |t|
t.references :fork_network, null: false, index: true, foreign_key: { on_delete: :cascade }
t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
t.references :forked_from_project, references: :projects
end
add_concurrent_foreign_key :fork_network_members, :projects,
column: :forked_from_project_id,
on_delete: :nullify
end
def down
if foreign_key_exists?(:fork_network_members, column: :forked_from_project_id)
remove_foreign_key :fork_network_members, column: :forked_from_project_id
end
drop_table :fork_network_members
end
end
......@@ -591,6 +591,15 @@
add_index "features", ["key"], name: "index_features_on_key", unique: true, using: :btree
create_table "fork_network_members", force: :cascade do |t|
t.integer "fork_network_id", null: false
t.integer "project_id", null: false
t.integer "forked_from_project_id"
end
add_index "fork_network_members", ["fork_network_id"], name: "index_fork_network_members_on_fork_network_id", using: :btree
add_index "fork_network_members", ["project_id"], name: "index_fork_network_members_on_project_id", unique: true, using: :btree
create_table "fork_networks", force: :cascade do |t|
t.integer "root_project_id"
t.string "deleted_root_project_name"
......@@ -1800,6 +1809,9 @@
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade
add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify
add_foreign_key "fork_network_members", "projects", on_delete: :cascade
add_foreign_key "fork_networks", "projects", column: "root_project_id", name: "fk_e7b436b2b5", on_delete: :nullify
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
add_foreign_key "gcp_clusters", "projects", on_delete: :cascade
......
require 'spec_helper'
describe ForkNetworkMember do
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:fork_network) }
end
end
require 'rails_helper'
require 'spec_helper'
RSpec.describe ForkNetwork, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
describe ForkNetwork do
include ProjectForksHelper
describe '#add_root_as_member' do
it 'adds the root project as a member when creating a new root network' do
project = create(:project)
fork_network = described_class.create(root_project: project)
expect(fork_network.projects).to include(project)
end
end
context 'for a deleted project' do
it 'keeps the fork network' do
project = create(:project, :public)
forked = fork_project(project)
project.destroy!
fork_network = forked.reload.fork_network
expect(fork_network.projects).to contain_exactly(forked)
expect(fork_network.root_project).to be_nil
end
it 'allows multiple fork networks where the root project is deleted' do
first_project = create(:project)
second_project = create(:project)
first_fork = fork_project(first_project)
second_fork = fork_project(second_project)
first_project.destroy
second_project.destroy
expect(first_fork.fork_network).not_to be_nil
expect(first_fork.fork_network.root_project).to be_nil
expect(second_fork.fork_network).not_to be_nil
expect(second_fork.fork_network.root_project).to be_nil
end
end
end
......@@ -60,6 +60,33 @@
expect(@from_project.forks_count).to eq(1)
end
it 'creates a fork network with the new project and the root project set' do
to_project
fork_network = @from_project.reload.fork_network
expect(fork_network).not_to be_nil
expect(fork_network.root_project).to eq(@from_project)
expect(fork_network.projects).to contain_exactly(@from_project, to_project)
end
end
context 'creating a fork of a fork' do
let(:from_forked_project) { fork_project(@from_project, @to_user) }
let(:other_namespace) do
group = create(:group)
group.add_owner(@to_user)
group
end
let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
it 'sets the root of the network to the root project' do
expect(to_project.fork_network.root_project).to eq(@from_project)
end
it 'sets the forked_from_project on the membership' do
expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project)
end
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