# frozen_string_literal: true module Projects class UpdateRemoteMirrorService < BaseService include Gitlab::Utils::StrongMemoize MAX_TRIES = 3 def execute(remote_mirror, tries) return success unless remote_mirror.enabled? # Blocked URLs are a hard failure, no need to attempt to retry if Gitlab::UrlBlocker.blocked_url?(normalized_url(remote_mirror.url), schemes: Project::VALID_MIRROR_PROTOCOLS) hard_retry_or_fail(remote_mirror, _('The remote mirror URL is invalid.'), tries) return error(remote_mirror.last_error) end update_mirror(remote_mirror) success rescue Gitlab::Git::CommandError => e # This happens if one of the gitaly calls above fail, for example when # branches have diverged, or the pre-receive hook fails. hard_retry_or_fail(remote_mirror, e.message, tries) error(e.message) rescue StandardError => e remote_mirror.hard_fail!(e.message) raise e end private def normalized_url(url) strong_memoize(:normalized_url) do CGI.unescape(Gitlab::UrlSanitizer.sanitize(url)) end end def update_mirror(remote_mirror) remote_mirror.update_start! # LFS objects must be sent first, or the push has dangling pointers lfs_status = send_lfs_objects!(remote_mirror) response = remote_mirror.update_repository failed, failure_message = failure_status(lfs_status, response, remote_mirror) # When the issue https://gitlab.com/gitlab-org/gitlab/-/issues/349262 is closed, # we can move this block within failure_status. if failed remote_mirror.mark_as_failed!(failure_message) else remote_mirror.update_finish! end end def failure_status(lfs_status, response, remote_mirror) message = '' failed = false lfs_sync_failed = false if lfs_status&.dig(:status) == :error lfs_sync_failed = true message += "Error synchronizing LFS files:" message += "\n\n#{lfs_status[:message]}\n\n" failed = Feature.enabled?(:remote_mirror_fail_on_lfs, project) end if response.divergent_refs.any? message += "Some refs have diverged and have not been updated on the remote:" message += "\n\n#{response.divergent_refs.join("\n")}" failed = true end if message.present? Gitlab::AppJsonLogger.info( message: "Error synching remote mirror", project_id: project.id, project_path: project.full_path, remote_mirror_id: remote_mirror.id, lfs_sync_failed: lfs_sync_failed, divergent_ref_list: response.divergent_refs ) end [failed, message] end def send_lfs_objects!(remote_mirror) return unless project.lfs_enabled? # TODO: Support LFS sync over SSH # https://gitlab.com/gitlab-org/gitlab/-/issues/249587 return unless remote_mirror.url =~ %r{\Ahttps?://}i return unless remote_mirror.password_auth? Lfs::PushService.new( project, current_user, url: remote_mirror.bare_url, credentials: remote_mirror.credentials ).execute end def hard_retry_or_fail(mirror, message, tries) if tries < MAX_TRIES mirror.hard_retry!(message) else # It's not likely we'll be able to recover from this ourselves, so we'll # notify the users of the problem, and don't trigger any sidekiq retries # Instead, we'll wait for the next change to try the push again, or until # a user manually retries. mirror.hard_fail!(message) end end end end