114 lines
4.5 KiB
Ruby
114 lines
4.5 KiB
Ruby
|
# 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
|