diff options
Diffstat (limited to 'app/services/projects/forks/sync_service.rb')
-rw-r--r-- | app/services/projects/forks/sync_service.rb | 113 |
1 files changed, 113 insertions, 0 deletions
diff --git a/app/services/projects/forks/sync_service.rb b/app/services/projects/forks/sync_service.rb new file mode 100644 index 00000000000..4c70d7f17f5 --- /dev/null +++ b/app/services/projects/forks/sync_service.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +module Projects + module Forks + # A service for fetching upstream default branch and merging it to the fork's specified branch. + class SyncService < BaseService + ONGOING_MERGE_ERROR = 'The synchronization did not happen due to another merge in progress' + + MergeError = Class.new(StandardError) + + def initialize(project, user, target_branch) + super(project, user) + + @source_project = project.fork_source + @head_sha = project.repository.commit(target_branch).sha + @target_branch = target_branch + @details = Projects::Forks::Details.new(project, target_branch) + end + + def execute + execute_service + + ServiceResponse.success + rescue MergeError => e + Gitlab::ErrorTracking.log_exception(e, { project_id: project.id, user_id: current_user.id }) + + ServiceResponse.error(message: e.message) + ensure + details.exclusive_lease.cancel + end + + private + + attr_reader :source_project, :head_sha, :target_branch, :details + + # The method executes multiple steps: + # + # 1. Gitlab::Git::CrossRepo fetches upstream default branch into a temporary ref and returns new source sha. + # 2. New divergence counts are calculated using the source sha. + # 3. If the fork is not behind, there is nothing to merge -> exit. + # 4. Otherwise, continue with the new source sha. + # 5. If Gitlab::Git::CommandError is raised it means that merge couldn't happen due to a merge conflict. The + # details are updated to transfer this error to the user. + def execute_service + counts = [] + source_sha = source_project.commit.sha + + Gitlab::Git::CrossRepo.new(repository, source_project.repository) + .execute(source_sha) do |cross_repo_source_sha| + counts = repository.diverging_commit_count(head_sha, cross_repo_source_sha) + ahead, behind = counts + next if behind == 0 + + execute_with_fetched_source(cross_repo_source_sha, ahead) + end + rescue Gitlab::Git::CommandError => e + details.update!({ sha: head_sha, source_sha: source_sha, counts: counts, has_conflicts: true }) + + raise MergeError, e.message + end + + def execute_with_fetched_source(cross_repo_source_sha, ahead) + with_linked_lfs_pointers(cross_repo_source_sha) do + merge_commit_id = perform_merge(cross_repo_source_sha, ahead) + raise MergeError, ONGOING_MERGE_ERROR unless merge_commit_id + end + end + + # This method merges the upstream default branch to the fork specified branch. + # Depending on whether the fork branch is ahead of upstream or not, a different type of + # merge is performed. + # + # If the fork's branch is not ahead of the upstream (only behind), fast-forward merge is performed. + # However, if the fork's branch contains commits that don't exist upstream, a merge commit is created. + # In this case, a conflict may happen, which interrupts the merge and returns a message to the user. + def perform_merge(cross_repo_source_sha, ahead) + if ahead > 0 + message = "Merge branch #{source_project.path}:#{source_project.default_branch} into #{target_branch}" + + repository.merge_to_branch(current_user, + source_sha: cross_repo_source_sha, + target_branch: target_branch, + target_sha: head_sha, + message: message) + else + repository.ff_merge(current_user, cross_repo_source_sha, target_branch, target_sha: head_sha) + end + end + + # This method links the newly merged lfs objects (if any) with the existing ones upstream. + # The LfsLinkService service has a limit and may raise an error if there are too many lfs objects to link. + # This is the reason why the block is passed: + # + # 1. Verify that there are not too many lfs objects to link + # 2. Execute the block (which basically performs the merge) + # 3. Link lfs objects + def with_linked_lfs_pointers(newrev, &block) + return yield unless project.lfs_enabled? + + oldrev = head_sha + new_lfs_oids = + Gitlab::Git::LfsChanges + .new(repository, newrev) + .new_pointers(not_in: [oldrev]) + .map(&:lfs_oid) + + Projects::LfsPointers::LfsLinkService.new(project).execute(new_lfs_oids, &block) + rescue Projects::LfsPointers::LfsLinkService::TooManyOidsError => e + raise MergeError, e.message + end + end + end +end |