diff options
80 files changed, 1368 insertions, 260 deletions
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 1096afbb798..99dc3dda9e7 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -120,10 +120,13 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap end def selected_target_project - if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil? + if @project.id.to_s == params[:target_project_id] || !@project.forked? @project + elsif params[:target_project_id].present? + MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project) + .execute.find(params[:target_project_id]) else - @project.forked_project_link.forked_from_project + @project.forked_from_project end end end diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb new file mode 100644 index 00000000000..189eb3847eb --- /dev/null +++ b/app/finders/merge_request_target_project_finder.rb @@ -0,0 +1,18 @@ +class MergeRequestTargetProjectFinder + attr_reader :current_user, :source_project + + def initialize(current_user: nil, source_project:) + @current_user = current_user + @source_project = source_project + end + + def execute + if @source_project.fork_network + @source_project.fork_network.projects + .public_or_visible_to_user(current_user) + .with_feature_available_for_user(:merge_requests, current_user) + else + Project.where(id: source_project) + end + end +end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index c31023f2d9a..5b2c58d193d 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -73,7 +73,8 @@ module MergeRequestsHelper end def target_projects(project) - [project, project.default_merge_request_target].uniq + MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project) + .execute end def merge_request_button_visibility(merge_request, closed) diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb new file mode 100644 index 00000000000..218e37a5312 --- /dev/null +++ b/app/models/fork_network.rb @@ -0,0 +1,15 @@ +class ForkNetwork < ActiveRecord::Base + 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 + + def find_forks_in(other_projects) + projects.where(id: other_projects) + end +end diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb new file mode 100644 index 00000000000..6a9b52a1ef8 --- /dev/null +++ b/app/models/fork_network_member.rb @@ -0,0 +1,7 @@ +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 diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 086226618e6..9b312f7db6c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -403,7 +403,7 @@ class MergeRequest < ActiveRecord::Base return false unless for_fork? return true unless source_project - !source_project.forked_from?(target_project) + !source_project.in_fork_network_of?(target_project) end def reopenable? diff --git a/app/models/namespace.rb b/app/models/namespace.rb index e279d8dd8c5..4672881e220 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -139,7 +139,9 @@ class Namespace < ActiveRecord::Base end def find_fork_of(project) - projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id) + return nil unless project.fork_network + + project.fork_network.find_forks_in(projects).first end def lfs_enabled? diff --git a/app/models/project.rb b/app/models/project.rb index 608b545f99f..ad1c339ae78 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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' @@ -1000,6 +1009,11 @@ class Project < ActiveRecord::Base end def forked? + return true if fork_network && fork_network.root_project != self + + # TODO: Use only the above conditional using the `fork_network` + # This is the old conditional that looks at the `forked_project_link`, we + # fall back to this while we're migrating the new models !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) end @@ -1119,8 +1133,19 @@ class Project < ActiveRecord::Base end end - def forked_from?(project) - forked? && project == forked_from_project + def forked_from?(other_project) + forked? && forked_from_project == other_project + end + + def in_fork_network_of?(other_project) + # TODO: Remove this in a next release when all fork_networks are populated + # This makes sure all MergeRequests remain valid while the projects don't + # have a fork_network yet. + return true if forked_from?(other_project) + + return false if fork_network.nil? || other_project.fork_network.nil? + + fork_network == other_project.fork_network end def origin_merge_requests diff --git a/app/models/user.rb b/app/models/user.rb index 959738ba608..c3f115ca074 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -697,15 +697,7 @@ class User < ActiveRecord::Base end def fork_of(project) - links = ForkedProjectLink.where( - forked_from_project_id: project, - forked_to_project_id: personal_projects.unscope(:order) - ) - if links.any? - links.first.forked_to_project - else - nil - end + namespace.find_fork_of(project) end def ldap_user? diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 54eb75ab9bf..19d75ff2efa 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -22,6 +22,13 @@ module Projects Projects::UnlinkForkService.new(project, current_user).execute + # The project is not necessarily a fork, so update the fork network originating + # from this project + if fork_network = project.root_of_fork_network + fork_network.update(root_project: nil, + deleted_root_project_name: project.full_name) + end + attempt_destroy_transaction(project) system_hook_service.execute_hooks_for(project, :destroy) diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index ad67e68a86a..eb5cce5ab98 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -23,11 +23,31 @@ module Projects 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 diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index f30b40423c8..abe414d0c05 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -16,6 +16,7 @@ module Projects refresh_forks_count(@project.forked_from_project) @project.forked_project_link.destroy + @project.fork_network_member.destroy end def refresh_forks_count(project) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 873b3045ea9..619b632918e 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,4 +1,6 @@ - empty_repo = @project.empty_repo? +- fork_network = @project.fork_network +- forked_from_project = @project.forked_from_project || fork_network&.root_project .project-home-panel.text-center{ class: ("empty-project" if empty_repo) } .limit-container-width{ class: container_class } .avatar-container.s70.project-avatar @@ -12,11 +14,15 @@ - if @project.description.present? = markdown_field(@project, :description) - - if forked_from_project = @project.forked_from_project + - if @project.forked? %p - #{ s_('ForkedFromProjectPath|Forked from') } - = link_to project_path(forked_from_project) do - = forked_from_project.namespace.try(:name) + - if forked_from_project + #{ s_('ForkedFromProjectPath|Forked from') } + = link_to project_path(forked_from_project) do + = forked_from_project.full_name + - else + - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') + = deleted_message % { project_name: fork_network.deleted_root_project_name } .project-repo-buttons .count-buttons diff --git a/changelogs/unreleased/bvl-fork-network-schema.yml b/changelogs/unreleased/bvl-fork-network-schema.yml new file mode 100644 index 00000000000..97b2d5acada --- /dev/null +++ b/changelogs/unreleased/bvl-fork-network-schema.yml @@ -0,0 +1,5 @@ +--- +title: Allow creating merge requests across a fork network +merge_request: 14422 +author: +type: changed diff --git a/db/migrate/20170928124105_create_fork_networks.rb b/db/migrate/20170928124105_create_fork_networks.rb new file mode 100644 index 00000000000..ca906b953a3 --- /dev/null +++ b/db/migrate/20170928124105_create_fork_networks.rb @@ -0,0 +1,28 @@ +class CreateForkNetworks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + create_table :fork_networks do |t| + t.references :root_project, + references: :projects, + index: { unique: true } + + t.string :deleted_root_project_name + end + + add_concurrent_foreign_key :fork_networks, :projects, + column: :root_project_id, + on_delete: :nullify + end + + def down + if foreign_keys_for(:fork_networks, :root_project_id).any? + remove_foreign_key :fork_networks, column: :root_project_id + end + drop_table :fork_networks + end +end diff --git a/db/migrate/20170928133643_create_fork_network_members.rb b/db/migrate/20170928133643_create_fork_network_members.rb new file mode 100644 index 00000000000..836f023efdc --- /dev/null +++ b/db/migrate/20170928133643_create_fork_network_members.rb @@ -0,0 +1,26 @@ +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_keys_for(:fork_network_members, :forked_from_project_id).any? + remove_foreign_key :fork_network_members, column: :forked_from_project_id + end + drop_table :fork_network_members + end +end diff --git a/db/migrate/20170929131201_populate_fork_networks.rb b/db/migrate/20170929131201_populate_fork_networks.rb new file mode 100644 index 00000000000..1214962770f --- /dev/null +++ b/db/migrate/20170929131201_populate_fork_networks.rb @@ -0,0 +1,30 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class PopulateForkNetworks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + MIGRATION = 'PopulateForkNetworksRange'.freeze + BATCH_SIZE = 100 + DELAY_INTERVAL = 15.seconds + + disable_ddl_transaction! + + class ForkedProjectLink < ActiveRecord::Base + include EachBatch + + self.table_name = 'forked_project_links' + end + + def up + say 'Populating the `fork_networks` based on existing `forked_project_links`' + + queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end + + def down + # nothing + end +end diff --git a/db/schema.rb b/db/schema.rb index 25aa15b837f..aac37b6b455 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -591,6 +591,22 @@ ActiveRecord::Schema.define(version: 20171006091000) do 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" + end + + add_index "fork_networks", ["root_project_id"], name: "index_fork_networks_on_root_project_id", unique: true, using: :btree + create_table "forked_project_links", force: :cascade do |t| t.integer "forked_to_project_id", null: false t.integer "forked_from_project_id", null: false @@ -1793,6 +1809,10 @@ ActiveRecord::Schema.define(version: 20171006091000) do 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 add_foreign_key "gcp_clusters", "services", on_delete: :nullify diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 3490bbd968c..60707f26aee 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -58,13 +58,13 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps step 'I should see my fork on the list' do page.within('.js-projects-list-holder') do - project = @user.fork_of(@project) + project = @user.fork_of(@project.reload) expect(page).to have_content("#{project.namespace.human_name} / #{project.name}") end end step 'I make forked repo invalid' do - project = @user.fork_of(@project) + project = @user.fork_of(@project.reload) project.path = 'test-crappy-path' project.save! end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 420ac8a695a..6781a906a94 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -5,6 +5,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps include SharedPaths include Select2Helper include WaitForRequests + include ProjectForksHelper step 'I am a member of project "Shop"' do @project = ::Project.find_by(name: "Shop") @@ -13,7 +14,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I have a project forked off of "Shop" called "Forked Shop"' do - @forked_project = Projects::ForkService.new(@project, @user).execute + @forked_project = fork_project(@project, @user, + namespace: @user.namespace, + repository: true) end step 'I click link "New Merge Request"' do diff --git a/features/support/env.rb b/features/support/env.rb index 608d988755c..5962745d501 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -10,7 +10,7 @@ if ENV['CI'] Knapsack::Adapters::SpinachAdapter.bind end -%w(select2_helper test_env repo_helpers wait_for_requests sidekiq).each do |f| +%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper).each do |f| require Rails.root.join('spec', 'support', f) end diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb new file mode 100644 index 00000000000..4b468e9cd58 --- /dev/null +++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb @@ -0,0 +1,60 @@ +module Gitlab + module BackgroundMigration + class CreateForkNetworkMembershipsRange + RESCHEDULE_DELAY = 15 + + class ForkedProjectLink < ActiveRecord::Base + self.table_name = 'forked_project_links' + end + + def perform(start_id, end_id) + log("Creating memberships for forks: #{start_id} - #{end_id}") + + ActiveRecord::Base.connection.execute <<~INSERT_MEMBERS + INSERT INTO fork_network_members (fork_network_id, project_id, forked_from_project_id) + + SELECT fork_network_members.fork_network_id, + forked_project_links.forked_to_project_id, + forked_project_links.forked_from_project_id + + FROM forked_project_links + + INNER JOIN fork_network_members + ON forked_project_links.forked_from_project_id = fork_network_members.project_id + + WHERE forked_project_links.id BETWEEN #{start_id} AND #{end_id} + AND NOT EXISTS ( + SELECT true + FROM fork_network_members existing_members + WHERE existing_members.project_id = forked_project_links.forked_to_project_id + ) + INSERT_MEMBERS + + if missing_members?(start_id, end_id) + BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [start_id, end_id]) + end + end + + def missing_members?(start_id, end_id) + count_sql = <<~MISSING_MEMBERS + SELECT COUNT(*) + + FROM forked_project_links + + WHERE NOT EXISTS ( + SELECT true + FROM fork_network_members + WHERE fork_network_members.project_id = forked_project_links.forked_to_project_id + ) + AND forked_project_links.id BETWEEN #{start_id} AND #{end_id} + MISSING_MEMBERS + + ForkNetworkMember.count_by_sql(count_sql) > 0 + end + + def log(message) + Rails.logger.info("#{self.class.name} - #{message}") + end + end + end +end diff --git a/lib/gitlab/background_migration/populate_fork_networks_range.rb b/lib/gitlab/background_migration/populate_fork_networks_range.rb new file mode 100644 index 00000000000..6c355ed1e75 --- /dev/null +++ b/lib/gitlab/background_migration/populate_fork_networks_range.rb @@ -0,0 +1,54 @@ +module Gitlab + module BackgroundMigration + class PopulateForkNetworksRange + def perform(start_id, end_id) + log("Creating fork networks for forked project links: #{start_id} - #{end_id}") + + ActiveRecord::Base.connection.execute <<~INSERT_NETWORKS + INSERT INTO fork_networks (root_project_id) + SELECT DISTINCT forked_project_links.forked_from_project_id + + FROM forked_project_links + + WHERE NOT EXISTS ( + SELECT true + FROM forked_project_links inner_links + WHERE inner_links.forked_to_project_id = forked_project_links.forked_from_project_id + ) + AND NOT EXISTS ( + SELECT true + FROM fork_networks + WHERE forked_project_links.forked_from_project_id = fork_networks.root_project_id + ) + AND forked_project_links.id BETWEEN #{start_id} AND #{end_id} + INSERT_NETWORKS + + log("Creating memberships for root projects: #{start_id} - #{end_id}") + + ActiveRecord::Base.connection.execute <<~INSERT_ROOT + INSERT INTO fork_network_members (fork_network_id, project_id) + SELECT DISTINCT fork_networks.id, fork_networks.root_project_id + + FROM fork_networks + + INNER JOIN forked_project_links + ON forked_project_links.forked_from_project_id = fork_networks.root_project_id + + WHERE NOT EXISTS ( + SELECT true + FROM fork_network_members + WHERE fork_network_members.project_id = fork_networks.root_project_id + ) + AND forked_project_links.id BETWEEN #{start_id} AND #{end_id} + INSERT_ROOT + + delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY + BackgroundMigrationWorker.perform_in(delay, "CreateForkNetworkMembershipsRange", [start_id, end_id]) + end + + def log(message) + Rails.logger.info("#{self.class.name} - #{message}") + end + end + end +end diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index 243c1f1394d..7e7aaeeaa17 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -23,7 +23,8 @@ module Gitlab @extractor.analyze(closing_statements.join(" ")) @extractor.issues.reject do |issue| - @extractor.project.forked_from?(issue.project) # Don't extract issues on original project + # Don't extract issues from the project this project was forked from + @extractor.project.forked_from?(issue.project) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7569af9d175..c73e582e608 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-10-04 23:47+0100\n" -"PO-Revision-Date: 2017-10-04 23:47+0100\n" +"POT-Creation-Date: 2017-10-06 18:33+0200\n" +"PO-Revision-Date: 2017-10-06 18:33+0200\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -23,6 +23,11 @@ msgid_plural "%d commits" msgstr[0] "" msgstr[1] "" +msgid "%d layer" +msgid_plural "%d layers" +msgstr[0] "" +msgstr[1] "" + msgid "%s additional commit has been omitted to prevent performance issues." msgid_plural "%s additional commits have been omitted to prevent performance issues." msgstr[0] "" @@ -59,6 +64,9 @@ msgstr[1] "" msgid "1st contribution!" msgstr "" +msgid "2FA enabled" +msgstr "" + msgid "A collection of graphs regarding Continuous Integration" msgstr "" @@ -528,6 +536,51 @@ msgstr "" msgid "Compare" msgstr "" +msgid "Container Registry" +msgstr "" + +msgid "ContainerRegistry|Created" +msgstr "" + +msgid "ContainerRegistry|First log in to GitLab’s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:" +msgstr "" + +msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:" +msgstr "" + +msgid "ContainerRegistry|How to use the Container Registry" +msgstr "" + +msgid "ContainerRegistry|Learn more about" +msgstr "" + +msgid "ContainerRegistry|No tags in Container Registry for this container image." +msgstr "" + +msgid "ContainerRegistry|Once you log in, you’re free to create and upload a container image using the common %{build} and %{push} commands" +msgstr "" + +msgid "ContainerRegistry|Remove repository" +msgstr "" + +msgid "ContainerRegistry|Remove tag" +msgstr "" + +msgid "ContainerRegistry|Size" +msgstr "" + +msgid "ContainerRegistry|Tag" +msgstr "" + +msgid "ContainerRegistry|Tag ID" +msgstr "" + +msgid "ContainerRegistry|Use different image names" +msgstr "" + +msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images." +msgstr "" + msgid "Contribution guide" msgstr "" @@ -739,6 +792,9 @@ msgstr[1] "" msgid "ForkedFromProjectPath|Forked from" msgstr "" +msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)" +msgstr "" + msgid "Format" msgstr "" @@ -949,6 +1005,9 @@ msgstr "" msgid "New tag" msgstr "" +msgid "No container images stored for this project. Add one by following the instructions above." +msgstr "" + msgid "No repository" msgstr "" @@ -1024,6 +1083,9 @@ msgstr "" msgid "OpenedNDaysAgo|Opened" msgstr "" +msgid "Opens in a new window" +msgstr "" + msgid "Options" msgstr "" @@ -1350,6 +1412,15 @@ msgstr[1] "" msgid "Snippets" msgstr "" +msgid "Something went wrong on our end." +msgstr "" + +msgid "Something went wrong while fetching the projects." +msgstr "" + +msgid "Something went wrong while fetching the registry list." +msgstr "" + msgid "SortOptions|Access level, ascending" msgstr "" @@ -1905,3 +1976,6 @@ msgid "parent" msgid_plural "parents" msgstr[0] "" msgstr[1] "" + +msgid "personal access token" +msgstr "" diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 64b9af7b845..fb76b7fdf38 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe Projects::BlobController do + include ProjectForksHelper + let(:project) { create(:project, :public, :repository) } describe "GET show" do @@ -226,9 +228,8 @@ describe Projects::BlobController do end context 'when user has forked project' do - let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) } - let!(:forked_project) { forked_project_link.forked_to_project } - let(:guest) { forked_project.owner } + let!(:forked_project) { fork_project(project, guest, namespace: guest.namespace, repository: true) } + let(:guest) { create(:user) } before do sign_in(guest) diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index fad2c8f3ab7..7260350d5fb 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::MergeRequests::DiffsController do + include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } @@ -37,12 +39,12 @@ describe Projects::MergeRequests::DiffsController do render_views let(:project) { create(:project, :repository) } - let(:fork_project) { create(:forked_project_with_submodules) } - let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + let(:forked_project) { fork_project_with_submodules(project) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } before do - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save + project.add_developer(user) + merge_request.reload go end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index e46d1995498..707e7c32283 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::MergeRequestsController do + include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } @@ -204,14 +206,11 @@ describe Projects::MergeRequestsController do context 'there is no source project' do let(:project) { create(:project, :repository) } - let(:fork_project) { create(:forked_project_with_submodules) } - let(:merge_request) { create(:merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + let(:forked_project) { fork_project_with_submodules(project) } + let!(:merge_request) { create(:merge_request, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } before do - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save - merge_request.reload - fork_project.destroy + forked_project.destroy end it 'closes MR without errors' do @@ -599,21 +598,16 @@ describe Projects::MergeRequestsController do describe 'GET ci_environments_status' do context 'the environment is from a forked project' do - let!(:forked) { create(:project, :repository) } + let!(:forked) { fork_project(project, user, repository: true) } let!(:environment) { create(:environment, project: forked) } let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } let(:admin) { create(:admin) } let(:merge_request) do - create(:forked_project_link, forked_to_project: forked, - forked_from_project: project) - create(:merge_request, source_project: forked, target_project: project) end before do - forked.team << [user, :master] - get :ci_environments_status, namespace_id: merge_request.project.namespace.to_param, project_id: merge_request.project, diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index e3114e5c06e..135fd6449ff 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::NotesController do + include ProjectForksHelper + let(:user) { create(:user) } let(:project) { create(:project) } let(:issue) { create(:issue, project: project) } @@ -210,18 +212,16 @@ describe Projects::NotesController do context 'when creating a commit comment from an MR fork' do let(:project) { create(:project, :repository) } - let(:fork_project) do - create(:project, :repository).tap do |fork| - create(:forked_project_link, forked_to_project: fork, forked_from_project: project) - end + let(:forked_project) do + fork_project(project, nil, repository: true) end let(:merge_request) do - create(:merge_request, source_project: fork_project, target_project: project, source_branch: 'feature', target_branch: 'master') + create(:merge_request, source_project: forked_project, target_project: project, source_branch: 'feature', target_branch: 'master') end let(:existing_comment) do - create(:note_on_commit, note: 'a note', project: fork_project, commit_id: merge_request.commit_shas.first) + create(:note_on_commit, note: 'a note', project: forked_project, commit_id: merge_request.commit_shas.first) end def post_create(extra_params = {}) @@ -231,7 +231,7 @@ describe Projects::NotesController do project_id: project, target_type: 'merge_request', target_id: merge_request.id, - note_project_id: fork_project.id, + note_project_id: forked_project.id, in_reply_to_discussion_id: existing_comment.discussion_id }.merge(extra_params) end @@ -253,16 +253,16 @@ describe Projects::NotesController do end context 'when the user has access to the fork' do - let(:discussion) { fork_project.notes.find_discussion(existing_comment.discussion_id) } + let(:discussion) { forked_project.notes.find_discussion(existing_comment.discussion_id) } before do - fork_project.add_developer(user) + forked_project.add_developer(user) existing_comment end it 'creates the note' do - expect { post_create }.to change { fork_project.notes.count }.by(1) + expect { post_create }.to change { forked_project.notes.count }.by(1) end end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 491f35d0fb6..d7148e888ab 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -1,6 +1,8 @@ require('spec_helper') describe ProjectsController do + include ProjectForksHelper + let(:project) { create(:project) } let(:public_project) { create(:project, :public) } let(:user) { create(:user) } @@ -377,10 +379,10 @@ describe ProjectsController do context "when the project is forked" do let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:forked_project) { fork_project(project, nil, repository: true) } let(:merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -388,7 +390,7 @@ describe ProjectsController do project.merge_requests << merge_request sign_in(admin) - delete :destroy, namespace_id: fork_project.namespace, id: fork_project + delete :destroy, namespace_id: forked_project.namespace, id: forked_project expect(merge_request.reload.state).to eq('closed') end @@ -455,18 +457,14 @@ describe ProjectsController do end context 'with forked project' do - let(:project_fork) { create(:project, :repository, namespace: user.namespace) } - - before do - create(:forked_project_link, forked_to_project: project_fork) - end + let(:forked_project) { fork_project(create(:project, :public), user) } it 'removes fork from project' do delete(:remove_fork, - namespace_id: project_fork.namespace.to_param, - id: project_fork.to_param, format: :js) + namespace_id: forked_project.namespace.to_param, + id: forked_project.to_param, format: :js) - expect(project_fork.forked?).to be_falsey + expect(forked_project.reload.forked?).to be_falsey expect(flash[:notice]).to eq('The fork relationship has been removed.') expect(response).to render_template(:remove_fork) end diff --git a/spec/factories/fork_networks.rb b/spec/factories/fork_networks.rb new file mode 100644 index 00000000000..f42d36f3d19 --- /dev/null +++ b/spec/factories/fork_networks.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :fork_network do + association :root_project, factory: :project + end +end diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 8204828b5b9..d26428ec286 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -3,12 +3,13 @@ require 'spec_helper' feature 'Dashboard Merge Requests' do include FilterItemSelectHelper include SortingHelper + include ProjectForksHelper let(:current_user) { create :user } let(:project) { create(:project) } let(:public_project) { create(:project, :public, :repository) } - let(:forked_project) { Projects::ForkService.new(public_project, current_user).execute } + let(:forked_project) { fork_project(public_project, current_user, repository: true) } before do project.add_master(current_user) diff --git a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb new file mode 100644 index 00000000000..93c40ff6443 --- /dev/null +++ b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +feature 'Creating a merge request from a fork', :js do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let!(:source_project) do + fork_project(project, user, + repository: true, + namespace: user.namespace) + end + + before do + source_project.add_master(user) + + sign_in(user) + end + + shared_examples 'create merge request to other project' do + it 'has all possible target projects' do + visit project_new_merge_request_path(source_project) + + first('.js-target-project').click + + within('.dropdown-target-project .dropdown-content') do + expect(page).to have_content(project.full_path) + expect(page).to have_content(target_project.full_path) + expect(page).to have_content(source_project.full_path) + end + end + + it 'allows creating the merge request to another target project' do + visit project_merge_requests_path(source_project) + + page.within '.content' do + click_link 'New merge request' + end + + find('.js-source-branch', match: :first).click + find('.dropdown-source-branch .dropdown-content a', match: :first).click + + first('.js-target-project').click + find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click + + click_button 'Compare branches and continue' + + wait_for_requests + + expect { click_button 'Submit merge request' } + .to change { target_project.merge_requests.reload.size }.by(1) + end + + it 'updates the branches when selecting a new target project' do + target_project_member = target_project.owner + CreateBranchService.new(target_project, target_project_member) + .execute('a-brand-new-branch-to-test', 'master') + visit project_new_merge_request_path(source_project) + + first('.js-target-project').click + find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click + + wait_for_requests + + first('.js-target-branch').click + + within('.dropdown-target-branch .dropdown-content') do + expect(page).to have_content('a-brand-new-branch-to-test') + end + end + end + + context 'creating to the source of a fork' do + let!(:target_project) { project } + + it_behaves_like('create merge request to other project') + end + + context 'creating to a sibling of a fork' do + let!(:target_project) do + other_user = create(:user) + fork_project(project, other_user, + repository: true, + namespace: other_user.namespace) + end + + it_behaves_like('create merge request to other project') + end +end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index 09541873f71..f9bc3ee6c58 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -1,21 +1,20 @@ require 'spec_helper' feature 'Merge request created from fork' do + include ProjectForksHelper + given(:user) { create(:user) } given(:project) { create(:project, :public, :repository) } - given(:fork_project) { create(:project, :public, :repository) } + given(:forked_project) { fork_project(project, user, repository: true) } given!(:merge_request) do - create(:forked_project_link, forked_to_project: fork_project, - forked_from_project: project) - - create(:merge_request_with_diffs, source_project: fork_project, + create(:merge_request_with_diffs, source_project: forked_project, target_project: project, description: 'Test merge request') end background do - fork_project.team << [user, :master] + forked_project.team << [user, :master] sign_in user end @@ -31,7 +30,7 @@ feature 'Merge request created from fork' do background do create(:note_on_commit, note: comment, - project: fork_project, + project: forked_project, commit_id: merge_request.commit_shas.first) end @@ -55,7 +54,7 @@ feature 'Merge request created from fork' do context 'source project is deleted' do background do MergeRequests::MergeService.new(project, user).execute(merge_request) - fork_project.destroy! + forked_project.destroy! end scenario 'user can access merge request', js: true do @@ -69,7 +68,7 @@ feature 'Merge request created from fork' do context 'pipeline present in source project' do given(:pipeline) do create(:ci_pipeline, - project: fork_project, + project: forked_project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) end diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index f4ba660ca39..ee9bb50a881 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Diffs URL', js: true do + include ProjectForksHelper + let(:project) { create(:project, :public, :repository) } let(:merge_request) { create(:merge_request, source_project: project) } @@ -68,7 +70,7 @@ feature 'Diffs URL', js: true do context 'when editing file' do let(:author_user) { create(:user) } let(:user) { create(:user) } - let(:forked_project) { Projects::ForkService.new(project, author_user).execute } + let(:forked_project) { fork_project(project, author_user, repository: true) } let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, target_project: project, author: author_user) } let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") } diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index de98b147d04..758fc9b139d 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -1,8 +1,10 @@ require 'rails_helper' describe 'New/edit merge request', :js do + include ProjectForksHelper + let!(:project) { create(:project, :public, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:forked_project) { fork_project(project, nil, repository: true) } let!(:user) { create(:user) } let!(:user2) { create(:user) } let!(:milestone) { create(:milestone, project: project) } @@ -170,16 +172,16 @@ describe 'New/edit merge request', :js do context 'forked project' do before do - fork_project.team << [user, :master] + forked_project.team << [user, :master] sign_in(user) end context 'new merge request' do before do visit project_new_merge_request_path( - fork_project, + forked_project, merge_request: { - source_project_id: fork_project.id, + source_project_id: forked_project.id, target_project_id: project.id, source_branch: 'fix', target_branch: 'master' @@ -238,7 +240,7 @@ describe 'New/edit merge request', :js do context 'edit merge request' do before do merge_request = create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project, source_branch: 'fix', target_branch: 'master' diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 1f9b52dd998..c9b8ef4e37b 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'issuable templates', js: true do + include ProjectForksHelper + let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } let(:issue_form_location) { '#content-body .issuable-details .detail-page-description' } @@ -116,15 +118,13 @@ feature 'issuable templates', js: true do context 'user creates a merge request from a forked project using templates' do let(:template_content) { 'this is a test "feature-proposal" template' } let(:fork_user) { create(:user) } - let(:fork_project) { create(:project, :public, :repository) } - let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project, target_project: project) } + let(:forked_project) { fork_project(project, fork_user) } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: forked_project, target_project: project) } background do sign_out(:user) project.team << [fork_user, :developer] - fork_project.team << [fork_user, :master] - create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project) sign_in(fork_user) diff --git a/spec/features/projects/user_creates_directory_spec.rb b/spec/features/projects/user_creates_directory_spec.rb index 1ba5d83eadf..e8f2e4813c5 100644 --- a/spec/features/projects/user_creates_directory_spec.rb +++ b/spec/features/projects/user_creates_directory_spec.rb @@ -79,7 +79,7 @@ feature 'User creates a directory', js: true do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Create directory') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) end diff --git a/spec/features/projects/user_creates_files_spec.rb b/spec/features/projects/user_creates_files_spec.rb index 3d335687510..51d918bc85d 100644 --- a/spec/features/projects/user_creates_files_spec.rb +++ b/spec/features/projects/user_creates_files_spec.rb @@ -142,7 +142,7 @@ describe 'User creates files' do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) expect(page).to have_content('New commit message') diff --git a/spec/features/projects/user_deletes_files_spec.rb b/spec/features/projects/user_deletes_files_spec.rb index 95cd316be0e..7f48a69d9b7 100644 --- a/spec/features/projects/user_deletes_files_spec.rb +++ b/spec/features/projects/user_deletes_files_spec.rb @@ -59,7 +59,7 @@ describe 'User deletes files' do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Delete file') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) expect(page).to have_content('New commit message') diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb index 19954313c23..2798041fa0c 100644 --- a/spec/features/projects/user_edits_files_spec.rb +++ b/spec/features/projects/user_edits_files_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'User edits files' do + include ProjectForksHelper let(:project) { create(:project, :repository, name: 'Shop') } let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) } @@ -122,7 +123,7 @@ describe 'User edits files' do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) @@ -130,5 +131,34 @@ describe 'User edits files' do expect(page).to have_content('New commit message') end + + context 'when the user already had a fork of the project', :js do + let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) } + before do + visit(project2_tree_path_root_ref) + end + + it 'links to the forked project for editing' do + click_link('.gitignore') + find('.js-edit-blob').click + + expect(page).not_to have_link('Fork') + expect(page).not_to have_button('Cancel') + + execute_script("ace.edit('editor').setValue('*.rbca')") + fill_in(:commit_message, with: 'Another commit', visible: true) + click_button('Commit changes') + + fork = user.fork_of(project2) + + expect(current_path).to eq(project_new_merge_request_path(fork)) + + wait_for_requests + + expect(page).to have_content('Another commit') + expect(page).to have_content("From #{forked_project.full_path}") + expect(page).to have_content("into #{project2.full_path}") + end + end end end diff --git a/spec/features/projects/user_replaces_files_spec.rb b/spec/features/projects/user_replaces_files_spec.rb index e284fdefd4f..a9628198d5b 100644 --- a/spec/features/projects/user_replaces_files_spec.rb +++ b/spec/features/projects/user_replaces_files_spec.rb @@ -74,7 +74,7 @@ describe 'User replaces files' do expect(page).to have_content('Replacement file commit message') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) diff --git a/spec/features/projects/user_uploads_files_spec.rb b/spec/features/projects/user_uploads_files_spec.rb index 98871317ca3..8014c299980 100644 --- a/spec/features/projects/user_uploads_files_spec.rb +++ b/spec/features/projects/user_uploads_files_spec.rb @@ -39,6 +39,9 @@ describe 'User uploads files' do expect(current_path).to eq(project_new_merge_request_path(project)) click_link('Changes') + find("a[data-action='diffs']", text: 'Changes').click + + wait_for_requests expect(page).to have_content('Lorem ipsum dolor sit amet') expect(page).to have_content('Sed ut perspiciatis unde omnis') @@ -51,7 +54,7 @@ describe 'User uploads files' do visit(project2_tree_path_root_ref) end - it 'uploads and commit a new fileto a forked project', js: true do + it 'uploads and commit a new file to a forked project', js: true do find('.add-to-tree').click click_link('Upload file') @@ -69,11 +72,13 @@ describe 'User uploads files' do expect(page).to have_content('New commit message') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) - click_link('Changes') + find("a[data-action='diffs']", text: 'Changes').click + + wait_for_requests expect(page).to have_content('Lorem ipsum dolor sit amet') expect(page).to have_content('Sed ut perspiciatis unde omnis') diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 81f7ab80a04..4b2c54d54b5 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Project' do + include ProjectForksHelper + describe 'creating from template' do let(:user) { create(:user) } let(:template) { Gitlab::ProjectTemplate.find(:rails) } @@ -57,11 +59,10 @@ feature 'Project' do describe 'remove forked relationship', js: true do let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { fork_project(create(:project, :public), user, namespace_id: user.namespace) } before do sign_in user - create(:forked_project_link, forked_to_project: project) visit edit_project_path(project) end @@ -71,11 +72,60 @@ feature 'Project' do remove_with_confirm('Remove fork relationship', project.path) expect(page).to have_content 'The fork relationship has been removed.' - expect(project.forked?).to be_falsey + expect(project.reload.forked?).to be_falsey expect(page).not_to have_content 'Remove fork relationship' end end + describe 'showing information about source of a project fork' do + let(:user) { create(:user) } + let(:base_project) { create(:project, :public, :repository) } + let(:forked_project) { fork_project(base_project, user, repository: true) } + + before do + sign_in user + end + + it 'shows a link to the source project when it is available' do + visit project_path(forked_project) + + expect(page).to have_content('Forked from') + expect(page).to have_link(base_project.full_name) + end + + it 'does not contain fork network information for the root project' do + forked_project + + visit project_path(base_project) + + expect(page).not_to have_content('In fork network of') + expect(page).not_to have_content('Forked from') + end + + it 'shows the name of the deleted project when the source was deleted' do + forked_project + Projects::DestroyService.new(base_project, base_project.owner).execute + + visit project_path(forked_project) + + expect(page).to have_content("Forked from #{base_project.full_name} (deleted)") + end + + context 'a fork of a fork' do + let(:fork_of_fork) { fork_project(forked_project, user, repository: true) } + + it 'links to the base project if the source project is removed' do + fork_of_fork + Projects::DestroyService.new(forked_project, user).execute + + visit project_path(fork_of_fork) + + expect(page).to have_content("Forked from") + expect(page).to have_link(base_project.full_name) + end + end + end + describe 'removal', js: true do let(:user) { create(:user, username: 'test', name: 'test') } let(:project) { create(:project, namespace: user.namespace, name: 'project1') } diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb new file mode 100644 index 00000000000..c81bfd7932c --- /dev/null +++ b/spec/finders/merge_request_target_project_finder_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe MergeRequestTargetProjectFinder do + include ProjectForksHelper + + let(:user) { create(:user) } + subject(:finder) { described_class.new(current_user: user, source_project: forked_project) } + + shared_examples 'finding related projects' do + it 'finds sibling projects and base project' do + other_fork + + expect(finder.execute).to contain_exactly(base_project, other_fork, forked_project) + end + + it 'does not include projects that have merge requests turned off' do + other_fork.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) + base_project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) + + expect(finder.execute).to contain_exactly(forked_project) + end + end + + context 'public projects' do + let(:base_project) { create(:project, :public, path: 'base') } + let(:forked_project) { fork_project(base_project) } + let(:other_fork) { fork_project(base_project) } + + it_behaves_like 'finding related projects' + end + + context 'private projects' do + let(:base_project) { create(:project, :private, path: 'base') } + let(:forked_project) { fork_project(base_project, base_project.owner) } + let(:other_fork) { fork_project(base_project, base_project.owner) } + + context 'when the user is a member of all projects' do + before do + base_project.add_developer(user) + forked_project.add_developer(user) + other_fork.add_developer(user) + end + + it_behaves_like 'finding related projects' + end + + it 'only finds the projects the user is a member of' do + other_fork.add_developer(user) + base_project.add_developer(user) + + expect(finder.execute).to contain_exactly(other_fork, base_project) + end + end +end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 95f445e7905..883bdf3746a 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -1,12 +1,18 @@ require 'spec_helper' describe MergeRequestsFinder do + include ProjectForksHelper + let(:user) { create :user } let(:user2) { create :user } - let(:project1) { create(:project) } - let(:project2) { create(:project, forked_from_project: project1) } - let(:project3) { create(:project, :archived, forked_from_project: project1) } + let(:project1) { create(:project, :public) } + let(:project2) { fork_project(project1, user) } + let(:project3) do + p = fork_project(project1, user) + p.update!(archived: true) + p + end let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') } diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 7d1c17909bf..fd7900c32f4 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe MergeRequestsHelper do + include ProjectForksHelper describe 'ci_build_details_path' do let(:project) { create(:project) } let(:merge_request) { MergeRequest.new } @@ -31,10 +32,10 @@ describe MergeRequestsHelper do describe 'within different projects' do let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } - let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) } + let(:forked_project) { fork_project(project) } + let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project) } subject { format_mr_branch_names(merge_request) } - let(:source_title) { "#{fork_project.full_path}:#{merge_request.source_branch}" } + let(:source_title) { "#{forked_project.full_path}:#{merge_request.source_branch}" } let(:target_title) { "#{project.full_path}:#{merge_request.target_branch}" } it { is_expected.to eq([source_title, target_title]) } diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb new file mode 100644 index 00000000000..1a4ea2bac48 --- /dev/null +++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migration, schema: 20170929131201 do + let(:migration) { described_class.new } + + let(:base1) { create(:project) } + let(:base1_fork1) { create(:project) } + let(:base1_fork2) { create(:project) } + + let(:base2) { create(:project) } + let(:base2_fork1) { create(:project) } + let(:base2_fork2) { create(:project) } + + let(:fork_of_fork) { create(:project) } + let(:fork_of_fork2) { create(:project) } + let(:second_level_fork) { create(:project) } + let(:third_level_fork) { create(:project) } + + let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) } + let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) } + + let!(:forked_project_links) { table(:forked_project_links) } + let!(:fork_networks) { table(:fork_networks) } + let!(:fork_network_members) { table(:fork_network_members) } + + before do + # The fork-network relation created for the forked project + fork_networks.create(id: 1, root_project_id: base1.id) + fork_network_members.create(project_id: base1.id, fork_network_id: 1) + fork_networks.create(id: 2, root_project_id: base2.id) + fork_network_members.create(project_id: base2.id, fork_network_id: 2) + + # Normal fork links + forked_project_links.create(id: 1, forked_from_project_id: base1.id, forked_to_project_id: base1_fork1.id) + forked_project_links.create(id: 2, forked_from_project_id: base1.id, forked_to_project_id: base1_fork2.id) + forked_project_links.create(id: 3, forked_from_project_id: base2.id, forked_to_project_id: base2_fork1.id) + forked_project_links.create(id: 4, forked_from_project_id: base2.id, forked_to_project_id: base2_fork2.id) + + # Fork links + forked_project_links.create(id: 5, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork.id) + forked_project_links.create(id: 6, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork2.id) + + # Forks 3 levels down + forked_project_links.create(id: 7, forked_from_project_id: fork_of_fork.id, forked_to_project_id: second_level_fork.id) + forked_project_links.create(id: 8, forked_from_project_id: second_level_fork.id, forked_to_project_id: third_level_fork.id) + + migration.perform(1, 8) + end + + it 'creates a memberships for the direct forks' do + base1_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1_fork1.id) + base1_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1_fork2.id) + base2_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2_fork1.id) + base2_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2_fork2.id) + + expect(base1_fork1_membership.forked_from_project_id).to eq(base1.id) + expect(base1_fork2_membership.forked_from_project_id).to eq(base1.id) + expect(base2_fork1_membership.forked_from_project_id).to eq(base2.id) + expect(base2_fork2_membership.forked_from_project_id).to eq(base2.id) + end + + it 'adds the fork network members for forks of forks' do + fork_of_fork_membership = fork_network_members.find_by(project_id: fork_of_fork.id, + fork_network_id: fork_network1.id) + fork_of_fork2_membership = fork_network_members.find_by(project_id: fork_of_fork2.id, + fork_network_id: fork_network1.id) + second_level_fork_membership = fork_network_members.find_by(project_id: second_level_fork.id, + fork_network_id: fork_network1.id) + third_level_fork_membership = fork_network_members.find_by(project_id: third_level_fork.id, + fork_network_id: fork_network1.id) + + expect(fork_of_fork_membership.forked_from_project_id).to eq(base1_fork1.id) + expect(fork_of_fork2_membership.forked_from_project_id).to eq(base1_fork1.id) + expect(second_level_fork_membership.forked_from_project_id).to eq(fork_of_fork.id) + expect(third_level_fork_membership.forked_from_project_id).to eq(second_level_fork.id) + end + + it 'reschedules itself when there are missing members' do + allow(migration).to receive(:missing_members?).and_return(true) + + expect(BackgroundMigrationWorker) + .to receive(:perform_in).with(described_class::RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [1, 3]) + + migration.perform(1, 3) + end + + it 'can be repeated without effect' do + expect { fork_network_members.count }.not_to change { migration.perform(1, 7) } + end + + it 'knows it is finished for this range' do + expect(migration.missing_members?(1, 7)).to be_falsy + end + + context 'with more forks' do + before do + forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) + forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) + end + + it 'only processes a single batch of links at a time' do + expect(fork_network_members.count).to eq(10) + + migration.perform(8, 10) + + expect(fork_network_members.count).to eq(12) + end + + it 'knows when not all memberships withing a batch have been created' do + expect(migration.missing_members?(8, 10)).to be_truthy + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb new file mode 100644 index 00000000000..3ef1873e615 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do + let(:migration) { described_class.new } + let(:base1) { create(:project) } + let(:base1_fork1) { create(:project) } + let(:base1_fork2) { create(:project) } + + let(:base2) { create(:project) } + let(:base2_fork1) { create(:project) } + let(:base2_fork2) { create(:project) } + + let!(:forked_project_links) { table(:forked_project_links) } + let!(:fork_networks) { table(:fork_networks) } + let!(:fork_network_members) { table(:fork_network_members) } + + let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) } + let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) } + + before do + # A normal fork link + forked_project_links.create(id: 1, + forked_from_project_id: base1.id, + forked_to_project_id: base1_fork1.id) + forked_project_links.create(id: 2, + forked_from_project_id: base1.id, + forked_to_project_id: base1_fork2.id) + + forked_project_links.create(id: 3, + forked_from_project_id: base2.id, + forked_to_project_id: base2_fork1.id) + forked_project_links.create(id: 4, + forked_from_project_id: base2_fork1.id, + forked_to_project_id: create(:project).id) + + forked_project_links.create(id: 5, + forked_from_project_id: base2.id, + forked_to_project_id: base2_fork2.id) + + migration.perform(1, 3) + end + + it 'it creates the fork network' do + expect(fork_network1).not_to be_nil + expect(fork_network2).not_to be_nil + end + + it 'does not create a fork network for a fork-of-fork' do + # perfrom the entire batch + migration.perform(1, 5) + + expect(fork_networks.find_by(root_project_id: base2_fork1.id)).to be_nil + end + + it 'creates memberships for the root of fork networks' do + base1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1.id) + base2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2.id) + + expect(base1_membership).not_to be_nil + expect(base2_membership).not_to be_nil + end + + it 'schedules a job for inserting memberships for forks-of-forks' do + delay = Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY + + expect(BackgroundMigrationWorker) + .to receive(:perform_in).with(delay, "CreateForkNetworkMembershipsRange", [1, 3]) + + migration.perform(1, 3) + end + + it 'only processes a single batch of links at a time' do + expect(fork_network_members.count).to eq(5) + + migration.perform(3, 5) + + expect(fork_network_members.count).to eq(7) + end + + it 'can be repeated without effect' do + expect { migration.perform(1, 3) }.not_to change { fork_network_members.count } + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index c457bda9dcb..29baa70d5ae 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -272,6 +272,9 @@ project: - uploads - members_and_requesters - build_trace_section_names +- root_of_fork_network +- fork_network_member +- fork_network award_emoji: - awardable - user diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index c7fbc2bc92f..dd0ce0dae41 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -1,13 +1,15 @@ require 'spec_helper' describe 'forked project import' do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } let!(:project) { create(:project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:forked_from_project) { create(:project, :repository) } - let(:fork_link) { create(:forked_project_link, forked_from_project: project_with_repo) } + let(:forked_project) { fork_project(project_with_repo, nil, repository: true) } let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } @@ -16,7 +18,7 @@ describe 'forked project import' do end let!(:merge_request) do - create(:merge_request, source_project: fork_link.forked_to_project, target_project: project_with_repo) + create(:merge_request, source_project: forked_project, target_project: project_with_repo) end let(:saver) do diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 4d87f27ce05..473ba40fae7 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -1,13 +1,14 @@ require 'spec_helper' describe Gitlab::ImportExport::MergeRequestParser do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } - let(:forked_from_project) { create(:project, :repository) } - let(:fork_link) { create(:forked_project_link, forked_from_project: project) } + let(:forked_project) { fork_project(project) } let!(:merge_request) do - create(:merge_request, source_project: fork_link.forked_to_project, target_project: project) + create(:merge_request, source_project: forked_project, target_project: project) end let(:parsed_merge_request) do diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 4c5efbde69a..e44a7c23452 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::SearchResults do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project) { create(:project, name: 'foo') } let!(:issue) { create(:issue, project: project, title: 'foo') } @@ -42,7 +44,7 @@ describe Gitlab::SearchResults do end it 'includes merge requests from source and target projects' do - forked_project = create(:project, forked_from_project: project) + forked_project = fork_project(project, user) merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo') results = described_class.new(user, Project.where(id: forked_project.id), 'foo') diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb index 862907c5d01..84c2e9f7e52 100644 --- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb +++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb') describe AddHeadPipelineForEachMergeRequest, :truncate do + include ProjectForksHelper + let(:migration) { described_class.new } let!(:project) { create(:project) } - let!(:forked_project_link) { create(:forked_project_link, forked_from_project: project) } - let!(:other_project) { forked_project_link.forked_to_project } + let!(:other_project) { fork_project(project) } let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: "branch_1") } let!(:pipeline_2) { create(:ci_pipeline, project: other_project, ref: "branch_1") } diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb new file mode 100644 index 00000000000..532ca1fca8c --- /dev/null +++ b/spec/models/fork_network_member_spec.rb @@ -0,0 +1,8 @@ +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 diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb new file mode 100644 index 00000000000..605ccd6db06 --- /dev/null +++ b/spec/models/fork_network_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +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 + + describe '#find_fork_in' do + it 'finds all fork of the current network in al collection' do + network = create(:fork_network) + root_project = network.root_project + another_project = fork_project(root_project) + create(:project) + + expect(network.find_forks_in(Project.all)) + .to contain_exactly(another_project, root_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 diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb index 7dbeb4d2e74..32e33e8f42f 100644 --- a/spec/models/forked_project_link_spec.rb +++ b/spec/models/forked_project_link_spec.rb @@ -1,10 +1,11 @@ require 'spec_helper' describe ForkedProjectLink, "add link on fork" do + include ProjectForksHelper + let(:project_from) { create(:project, :repository) } let(:project_to) { fork_project(project_from, user) } let(:user) { create(:user) } - let(:namespace) { user.namespace } before do project_from.add_reporter(user) @@ -64,13 +65,4 @@ describe ForkedProjectLink, "add link on fork" do expect(ForkedProjectLink.exists?(id: forked_project_link.id)).to eq(false) end end - - def fork_project(from_project, user) - service = Projects::ForkService.new(from_project, user) - shell = double('gitlab_shell', fork_repository: true) - - allow(service).to receive(:gitlab_shell).and_return(shell) - - service.execute - end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 188a0a98ec3..950af653c80 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe MergeRequest do include RepoHelpers + include ProjectForksHelper subject { create(:merge_request) } @@ -49,6 +50,19 @@ describe MergeRequest do expect(subject).to be_valid end end + + context 'for forks' do + let(:project) { create(:project) } + let(:fork1) { fork_project(project) } + let(:fork2) { fork_project(project) } + + it 'allows merge requests for sibling-forks' do + subject.source_project = fork1 + subject.target_project = fork2 + + expect(subject).to be_valid + end + end end describe 'respond to' do @@ -672,7 +686,7 @@ describe MergeRequest do describe '#diverged_commits_count' do let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:forked_project) { fork_project(project, nil, repository: true) } context 'when the target branch does not exist anymore' do subject { create(:merge_request, source_project: project, target_project: project) } @@ -700,7 +714,7 @@ describe MergeRequest do end context 'diverged on fork' do - subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) } + subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: forked_project, target_project: project) } it 'counts commits that are on target branch but not on source branch' do expect(subject.diverged_commits_count).to eq(29) @@ -708,7 +722,7 @@ describe MergeRequest do end context 'rebased on fork' do - subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: fork_project, target_project: project) } + subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: forked_project, target_project: project) } it 'counts commits that are on target branch but not on source branch' do expect(subject.diverged_commits_count).to eq(0) @@ -1257,11 +1271,7 @@ describe MergeRequest do end context 'with environments on source project' do - let(:source_project) do - create(:project, :repository) do |fork_project| - fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - end - end + let(:source_project) { fork_project(project, nil, repository: true) } let(:merge_request) do create(:merge_request, @@ -1425,14 +1435,14 @@ describe MergeRequest do describe "#source_project_missing?" do let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } + let(:forked_project) { fork_project(project) } let(:user) { create(:user) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } context "when the fork exists" do let(:merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1446,9 +1456,9 @@ describe MergeRequest do end context "when the fork does not exist" do - let(:merge_request) do + let!(:merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1471,14 +1481,14 @@ describe MergeRequest do describe "#closed_without_fork?" do let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } + let(:forked_project) { fork_project(project) } let(:user) { create(:user) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } context "when the merge request is closed" do let(:closed_merge_request) do create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1497,7 +1507,7 @@ describe MergeRequest do context "when the merge request is open" do let(:open_merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1516,24 +1526,24 @@ describe MergeRequest do end context 'forked project' do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:user) { create(:user) } - let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) } + let(:forked_project) { fork_project(project, user) } let!(:merge_request) do create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end it 'returns false if unforked' do - Projects::UnlinkForkService.new(fork_project, user).execute + Projects::UnlinkForkService.new(forked_project, user).execute expect(merge_request.reload.reopenable?).to be_falsey end it 'returns false if the source project is deleted' do - Projects::DestroyService.new(fork_project, user).execute + Projects::DestroyService.new(forked_project, user).execute expect(merge_request.reload.reopenable?).to be_falsey end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 3ea614776ca..2ebf6acd42a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Namespace do + include ProjectForksHelper + let!(:namespace) { create(:namespace) } describe 'associations' do @@ -520,4 +522,25 @@ describe Namespace do end end end + + describe '#has_forks_of?' do + let(:project) { create(:project, :public) } + let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) } + + before do + # Reset the fork network relation + project.reload + end + + it 'knows if there is a direct fork in the namespace' do + expect(namespace.find_fork_of(project)).to eq(forked_project) + end + + it 'knows when there is as fork-of-fork in the namespace' do + other_namespace = create(:namespace) + other_fork = fork_project(forked_project, other_namespace.owner, namespace: other_namespace) + + expect(other_namespace.find_fork_of(project)).to eq(other_fork) + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 760ea7da322..f7155369666 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1818,6 +1818,59 @@ describe Project do end end + context 'forks' do + include ProjectForksHelper + + let(:project) { create(:project, :public) } + let!(:forked_project) { fork_project(project) } + + describe '#fork_network' do + it 'includes a fork of the project' do + expect(project.fork_network.projects).to include(forked_project) + end + + it 'includes a fork of a fork' do + other_fork = fork_project(forked_project) + + expect(project.fork_network.projects).to include(other_fork) + end + + it 'includes sibling forks' do + other_fork = fork_project(project) + + expect(forked_project.fork_network.projects).to include(other_fork) + end + + it 'includes the base project' do + expect(forked_project.fork_network.projects).to include(project.reload) + end + end + + describe '#in_fork_network_of?' do + it 'is true for a real fork' do + expect(forked_project.in_fork_network_of?(project)).to be_truthy + end + + it 'is true for a fork of a fork', :postgresql do + other_fork = fork_project(forked_project) + + expect(other_fork.in_fork_network_of?(project)).to be_truthy + end + + it 'is true for sibling forks' do + sibling = fork_project(project) + + expect(sibling.in_fork_network_of?(forked_project)).to be_truthy + end + + it 'is false when another project is given' do + other_project = build_stubbed(:project) + + expect(forked_project.in_fork_network_of?(other_project)).to be_falsy + end + end + end + describe '#pushes_since_gc' do let(:project) { create(:project) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 995211845ce..ece6968dde6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe User do include Gitlab::CurrentSettings + include ProjectForksHelper describe 'modules' do subject { described_class } @@ -1431,7 +1432,7 @@ describe User do describe "#contributed_projects" do subject { create(:user) } let!(:project1) { create(:project) } - let!(:project2) { create(:project, forked_from_project: project3) } + let!(:project2) { fork_project(project3) } let!(:project3) { create(:project) } let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) } let!(:push_event) { create(:push_event, project: project1, author: subject) } @@ -1455,6 +1456,23 @@ describe User do end end + describe '#fork_of' do + let(:user) { create(:user) } + + it "returns a user's fork of a project" do + project = create(:project, :public) + user_fork = fork_project(project, user, namespace: user.namespace) + + expect(user.fork_of(project)).to eq(user_fork) + end + + it 'returns nil if the project does not have a fork network' do + project = create(:project) + + expect(user.fork_of(project)).to be_nil + end + end + describe '#can_be_removed?' do subject { create(:user) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index c4f6e97b915..5e66e1607ba 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe API::MergeRequests do + include ProjectForksHelper + let(:base_time) { Time.now } let(:user) { create(:user) } let(:admin) { create(:user, :admin) } @@ -616,17 +618,17 @@ describe API::MergeRequests do context 'forked projects' do let!(:user2) { create(:user) } - let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } + let!(:forked_project) { fork_project(project, user2) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } before do - fork_project.add_reporter(user2) + forked_project.add_reporter(user2) allow_any_instance_of(MergeRequest).to receive(:write_ref) end it "returns merge_request" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' expect(response).to have_gitlab_http_status(201) @@ -635,10 +637,10 @@ describe API::MergeRequests do end it "does not return 422 when source_branch equals target_branch" do - expect(project.id).not_to eq(fork_project.id) - expect(fork_project.forked?).to be_truthy - expect(fork_project.forked_from_project).to eq(project) - post api("/projects/#{fork_project.id}/merge_requests", user2), + expect(project.id).not_to eq(forked_project.id) + expect(forked_project.forked?).to be_truthy + expect(forked_project.forked_from_project).to eq(project) + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') @@ -647,7 +649,7 @@ describe API::MergeRequests do it 'returns 422 when target project has disabled merge requests' do project.project_feature.update(merge_requests_access_level: 0) - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test', target_branch: 'master', source_branch: 'markdown', @@ -658,36 +660,26 @@ describe API::MergeRequests do end it "returns 400 when source_branch is missing" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end context 'when target_branch is specified' do - it 'returns 422 if not a forked project' do - post api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user, - target_project_id: fork_project.id - expect(response).to have_gitlab_http_status(422) - end - it 'returns 422 if targeting a different fork' do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', @@ -698,8 +690,8 @@ describe API::MergeRequests do end it "returns 201 when target_branch is specified and for the same project" do - post api("/projects/#{fork_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id + post api("/projects/#{forked_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id expect(response).to have_gitlab_http_status(201) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 18f6f7df1fa..5964244f8c5 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -64,9 +64,12 @@ describe API::Projects do create(:project, :public) end + # TODO: We're currently querying to detect if a project is a fork + # in 2 ways. Lower this back to 8 when `ForkedProjectLink` relation is + # removed expect do get api('/projects', current_user) - end.not_to exceed_query_limit(control).with_threshold(8) + end.not_to exceed_query_limit(control).with_threshold(9) end end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index 86f38dd4ec1..df73c731c96 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe API::MergeRequests do + include ProjectForksHelper + let(:base_time) { Time.now } let(:user) { create(:user) } let(:admin) { create(:user, :admin) } @@ -312,17 +314,17 @@ describe API::MergeRequests do context 'forked projects' do let!(:user2) { create(:user) } - let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } + let!(:forked_project) { fork_project(project, user2) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } before do - fork_project.add_reporter(user2) + forked_project.add_reporter(user2) allow_any_instance_of(MergeRequest).to receive(:write_ref) end it "returns merge_request" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' expect(response).to have_gitlab_http_status(201) @@ -331,10 +333,10 @@ describe API::MergeRequests do end it "does not return 422 when source_branch equals target_branch" do - expect(project.id).not_to eq(fork_project.id) - expect(fork_project.forked?).to be_truthy - expect(fork_project.forked_from_project).to eq(project) - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + expect(project.id).not_to eq(forked_project.id) + expect(forked_project.forked?).to be_truthy + expect(forked_project.forked_from_project).to eq(project) + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') @@ -343,7 +345,7 @@ describe API::MergeRequests do it "returns 422 when target project has disabled merge requests" do project.project_feature.update(merge_requests_access_level: 0) - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test', target_branch: "master", source_branch: 'markdown', @@ -354,36 +356,26 @@ describe API::MergeRequests do end it "returns 400 when source_branch is missing" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end context 'when target_branch is specified' do - it 'returns 422 if not a forked project' do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user, - target_project_id: fork_project.id - expect(response).to have_gitlab_http_status(422) - end - it 'returns 422 if targeting a different fork' do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', @@ -394,8 +386,8 @@ describe API::MergeRequests do end it "returns 201 when target_branch is specified and for the same project" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id expect(response).to have_gitlab_http_status(201) end end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 27d09b8202e..badc8bccf84 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe 'Git LFS API and storage' do include WorkhorseHelpers + include ProjectForksHelper let(:user) { create(:user) } let!(:lfs_object) { create(:lfs_object, :with_file) } @@ -1173,11 +1174,6 @@ describe 'Git LFS API and storage' do ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token) end - def fork_project(project, user, object = nil) - allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) - Projects::ForkService.new(project, user, {}).execute - end - def post_lfs_json(url, body = nil, headers = nil) post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json')) end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index 5b7822d5d8e..f6bd6e9ede4 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe BuildDetailsEntity do + include ProjectForksHelper + set(:user) { create(:admin) } it 'inherits from JobEntity' do @@ -56,18 +58,16 @@ describe BuildDetailsEntity do end context 'when merge request is from a fork' do - let(:fork_project) do - create(:project, forked_from_project: project) - end + let(:forked_project) { fork_project(project) } - let(:pipeline) { create(:ci_pipeline, project: fork_project) } + let(:pipeline) { create(:ci_pipeline, project: forked_project) } before do allow(build).to receive(:merge_request).and_return(merge_request) end let(:merge_request) do - create(:merge_request, source_project: fork_project, + create(:merge_request, source_project: forked_project, target_project: project, source_branch: build.ref) end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index eb6e683cc23..08847183bf4 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Ci::CreatePipelineService do + include ProjectForksHelper + set(:project) { create(:project, :repository) } let(:user) { create(:admin) } let(:ref_name) { 'refs/heads/master' } @@ -82,13 +84,9 @@ describe Ci::CreatePipelineService do end context 'when merge request target project is different from source project' do + let!(:project) { fork_project(target_project, nil, repository: true) } let!(:target_project) { create(:project, :repository) } - let!(:forked_project_link) do - create(:forked_project_link, forked_to_project: project, - forked_from_project: target_project) - end - it 'updates head pipeline for merge request' do merge_request = create(:merge_request, source_branch: 'master', target_branch: "branch_1", diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb index 03c682ae0d7..5a9eb359ee1 100644 --- a/spec/services/delete_merged_branches_service_spec.rb +++ b/spec/services/delete_merged_branches_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe DeleteMergedBranchesService do + include ProjectForksHelper + subject(:service) { described_class.new(project, project.owner) } let(:project) { create(:project, :repository) } @@ -50,9 +52,9 @@ describe DeleteMergedBranchesService do context 'open merge requests' do it 'does not delete branches from open merge requests' do - fork_link = create(:forked_project_link, forked_from_project: project) + forked_project = fork_project(project) create(:merge_request, :opened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master') - create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master') + create(:merge_request, :opened, source_project: forked_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master') service.execute diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb index 6f49a65d795..9c9b0c4c4a1 100644 --- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb +++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb @@ -1,14 +1,16 @@ require 'spec_helper' describe MergeRequests::Conflicts::ResolveService do + include ProjectForksHelper let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - let(:fork_project) do - create(:forked_project_with_submodules) do |fork_project| - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save - end + let(:project) { create(:project, :public, :repository) } + + let(:forked_project) do + forked_project = fork_project(project, user) + TestEnv.copy_repo(forked_project, + bare_repo: TestEnv.forked_repo_path_bare, + refs: TestEnv::FORKED_BRANCH_SHA) + forked_project end let(:merge_request) do @@ -19,7 +21,7 @@ describe MergeRequests::Conflicts::ResolveService do let(:merge_request_from_fork) do create(:merge_request, - source_branch: 'conflict-resolvable-fork', source_project: fork_project, + source_branch: 'conflict-resolvable-fork', source_project: forked_project, target_branch: 'conflict-start', target_project: project) end @@ -114,7 +116,7 @@ describe MergeRequests::Conflicts::ResolveService do end it 'gets conflicts from the source project' do - expect(fork_project.repository.rugged).to receive(:merge_commits).and_call_original + expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original expect(project.repository.rugged).not_to receive(:merge_commits) resolve_conflicts diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 25599dea19f..274624aa8bb 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe MergeRequests::GetUrlsService do + include ProjectForksHelper + let(:project) { create(:project, :public, :repository) } let(:service) { described_class.new(project) } let(:source_branch) { "merge-test" } @@ -85,7 +87,7 @@ describe MergeRequests::GetUrlsService do context 'pushing to existing branch from forked project' do let(:user) { create(:user) } - let!(:forked_project) { Projects::ForkService.new(project, user).execute } + let!(:forked_project) { fork_project(project, user, repository: true) } let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) } let(:changes) { existing_branch_changes } # Source project is now the forked one diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 64e676f22a0..62dbe362ec8 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe MergeRequests::RefreshService do + include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:service) { described_class } @@ -12,7 +14,8 @@ describe MergeRequests::RefreshService do group.add_owner(@user) @project = create(:project, :repository, namespace: group) - @fork_project = Projects::ForkService.new(@project, @user).execute + @fork_project = fork_project(@project, @user, repository: true) + @merge_request = create(:merge_request, source_project: @project, source_branch: 'master', @@ -311,8 +314,7 @@ describe MergeRequests::RefreshService do context 'when the merge request is sourced from a different project' do it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do - forked_project = create(:project, :repository) - create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project) + forked_project = fork_project(@project, @user, repository: true) merge_request = create(:merge_request, target_branch: 'master', diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index c867139d1de..c90bad46295 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -212,6 +212,19 @@ describe Projects::DestroyService do end end + context 'as the root of a fork network' do + let!(:fork_network) { create(:fork_network, root_project: project) } + + it 'updates the fork network with the project name' do + destroy_project(project, user) + + fork_network.reload + + expect(fork_network.deleted_root_project_name).to eq(project.full_name) + expect(fork_network.root_project).to be_nil + end + end + def destroy_project(project, user, params = {}) if async Projects::DestroyService.new(project, user, params).async_execute diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index fa9d6969830..53862283a27 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Projects::ForkService do + include ProjectForksHelper let(:gitlab_shell) { Gitlab::Shell.new } describe 'fork by user' do @@ -33,7 +34,7 @@ describe Projects::ForkService do end describe "successfully creates project in the user namespace" do - let(:to_project) { fork_project(@from_project, @to_user) } + let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) } it { expect(to_project).to be_persisted } it { expect(to_project.errors).to be_empty } @@ -60,13 +61,40 @@ describe Projects::ForkService do 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 context 'project already exists' do it "fails due to validation, not transaction failure" do @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) - @to_project = fork_project(@from_project, @to_user) + @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace) expect(@existing_project).to be_persisted expect(@to_project).not_to be_persisted @@ -88,7 +116,7 @@ describe Projects::ForkService do end it 'does not allow creation' do - to_project = fork_project(@from_project, @to_user) + to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace) expect(to_project).not_to be_persisted expect(to_project.errors.messages).to have_key(:base) @@ -182,9 +210,4 @@ describe Projects::ForkService do end end end - - def fork_project(from_project, user, params = {}) - allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) - Projects::ForkService.new(from_project, user, params).execute - end end diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 4f1ab697460..50d3a4ec982 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -1,19 +1,22 @@ require 'spec_helper' describe Projects::UnlinkForkService do - subject { described_class.new(fork_project, user) } + include ProjectForksHelper - let(:fork_link) { create(:forked_project_link) } - let(:fork_project) { fork_link.forked_to_project } + subject { described_class.new(forked_project, user) } + + let(:fork_link) { forked_project.forked_project_link } + let(:project) { create(:project, :public) } + let(:forked_project) { fork_project(project, user) } let(:user) { create(:user) } context 'with opened merge request on the source project' do - let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) } - let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) } + let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: fork_link.forked_from_project) } + let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) } before do allow(MergeRequests::CloseService).to receive(:new) - .with(fork_project, user) + .with(forked_project, user) .and_return(mr_close_service) end @@ -25,13 +28,24 @@ describe Projects::UnlinkForkService do end it 'remove fork relation' do - expect(fork_project.forked_project_link).to receive(:destroy) + expect(forked_project.forked_project_link).to receive(:destroy) + + subject.execute + end + + it 'removes the link to the fork network' do + expect(forked_project.fork_network_member).to be_present + expect(forked_project.fork_network).to be_present subject.execute + forked_project.reload + + expect(forked_project.fork_network_member).to be_nil + expect(forked_project.reload.fork_network).to be_nil end it 'refreshes the forks count cache of the source project' do - source = fork_project.forked_from_project + source = forked_project.forked_from_project expect(source.forks_count).to eq(1) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index d400304622e..3da222e2ed8 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::UpdateService, '#execute' do + include ProjectForksHelper + let(:gitlab_shell) { Gitlab::Shell.new } let(:user) { create(:user) } let(:admin) { create(:admin) } @@ -76,13 +78,7 @@ describe Projects::UpdateService, '#execute' do describe 'when updating project that has forks' do let(:project) { create(:project, :internal) } - let(:forked_project) { create(:forked_project_with_submodules, :internal) } - - before do - forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, - forked_from_project_id: project.id) - forked_project.save - end + let(:forked_project) { fork_project(project) } it 'updates forks visibility level when parent set to more restrictive' do opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } diff --git a/spec/support/project_forks_helper.rb b/spec/support/project_forks_helper.rb new file mode 100644 index 00000000000..0d1c6792d13 --- /dev/null +++ b/spec/support/project_forks_helper.rb @@ -0,0 +1,58 @@ +module ProjectForksHelper + def fork_project(project, user = nil, params = {}) + # Load the `fork_network` for the project to fork as there might be one that + # wasn't loaded yet. + project.reload unless project.fork_network + + unless user + user = create(:user) + project.add_developer(user) + end + + unless params[:namespace] || params[:namespace_id] + params[:namespace] = create(:group) + params[:namespace].add_owner(user) + end + + service = Projects::ForkService.new(project, user, params) + + create_repository = params.delete(:repository) + # Avoid creating a repository + unless create_repository + allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) + shell = double('gitlab_shell', fork_repository: true) + allow(service).to receive(:gitlab_shell).and_return(shell) + end + + forked_project = service.execute + + # Reload the both projects so they know about their newly created fork_network + if forked_project.persisted? + project.reload + forked_project.reload + end + + if create_repository + # The call to project.repository.after_import in RepositoryForkWorker does + # not reset the @exists variable of this forked_project.repository + # so we have to explicitely call this method to clear the @exists variable. + # of the instance we're returning here. + forked_project.repository.after_import + + # We can't leave the hooks in place after a fork, as those would fail in tests + # The "internal" API is not available + FileUtils.rm_rf("#{forked_project.repository.path}/hooks") + end + + forked_project + end + + def fork_project_with_submodules(project, user = nil, params = {}) + forked_project = fork_project(project, user, params) + TestEnv.copy_repo(forked_project, + bare_repo: TestEnv.forked_repo_path_bare, + refs: TestEnv::FORKED_BRANCH_SHA) + + forked_project + end +end diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb index 98c7de9b709..efed2e02a1b 100644 --- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' describe 'projects/merge_requests/_commits.html.haml' do include Devise::Test::ControllerHelpers + include ProjectForksHelper let(:user) { create(:user) } - let(:target_project) { create(:project, :repository) } - let(:source_project) { create(:project, :repository, forked_from_project: target_project) } + let(:target_project) { create(:project, :public, :repository) } + let(:source_project) { fork_project(target_project, user, repository: true) } let(:merge_request) do create(:merge_request, :simple, diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index 69c7d0cbf28..9b74a7e1946 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -2,16 +2,19 @@ require 'spec_helper' describe 'projects/merge_requests/edit.html.haml' do include Devise::Test::ControllerHelpers + include ProjectForksHelper let(:user) { create(:user) } let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:forked_project) { fork_project(project, user, repository: true) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } let(:milestone) { create(:milestone, project: project) } let(:closed_merge_request) do + project.add_developer(user) + create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project, author: user, assignee: user, diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 6f29d12373a..28d54c2fb77 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -2,16 +2,17 @@ require 'spec_helper' describe 'projects/merge_requests/show.html.haml' do include Devise::Test::ControllerHelpers + include ProjectForksHelper let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:project) { create(:project, :public, :repository) } + let(:forked_project) { fork_project(project, user, repository: true) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) } let(:closed_merge_request) do create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project, author: user) end @@ -52,7 +53,7 @@ describe 'projects/merge_requests/show.html.haml' do context 'when the merge request is open' do it 'closes the merge request if the source project does not exist' do closed_merge_request.update_attributes(state: 'open') - fork_project.destroy + forked_project.destroy render diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb index 20cf580af8a..ed8cedc0079 100644 --- a/spec/workers/namespaceless_project_destroy_worker_spec.rb +++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe NamespacelessProjectDestroyWorker do + include ProjectForksHelper + subject { described_class.new } before do @@ -55,9 +57,11 @@ describe NamespacelessProjectDestroyWorker do context 'project forked from another' do let!(:parent_project) { create(:project) } - - before do - create(:forked_project_link, forked_to_project: project, forked_from_project: parent_project) + let(:project) do + namespaceless_project = fork_project(parent_project) + namespaceless_project.namespace_id = nil + namespaceless_project.save(validate: false) + namespaceless_project end it 'closes open merge requests' do |