Commit b9fd6118 authored by Georges Racinet's avatar Georges Racinet 🦑
Browse files

Merge Request from topics detection using Mercurial prune reasons

Expecting the new payload sent by Mercurial, this performs an
early treatment of published topics, before workers for each
branch get spawned.

In the most common case where the topic matches the target (named)
branch, we don't need any repository access, so this is each and
doesn't justify spreading with a worker per branch, as the general
GitLab detection will do.

The new HgManuallyMergedService takes care of updating the MR to
the latest relevant topical changeset, to mark it as merged and
perform all subsequent needed operations.

branch : heptapod-stable
parent f9732f09990f
# frozen_string_literal: true
module MergeRequests
# Service for manually merged Mercurial Merge Requests
# - update the MR to the latest relevant changeset
# - mark it as merged
# - execute all hooks and notifications
class HgManuallyMergedService < MergeRequests::BaseService
def execute(merge_request, latest_sha)
refresh(merge_request, latest_sha), @current_user).execute(merge_request)
# Refresh the MR to include a new commit, without reloading everything.
# Typically, with topics publication, the source branch has already vanished
# reloading everything would not understand that.
def refresh(merge_request, real_source_sha)
recorded_source_sha = merge_request.diff_head_sha
return if recorded_source_sha == real_source_sha
# the new diff creation apparently makes several Gitaly calls,
# which can be an error depending on running options (probably not in
# production, but that may change in GitLab versions). It's not the
# responsibility of this service to avoid that.
Gitlab::GitalyClient.allow_n_plus_1_calls do
old_diff_refs = merge_request.diff_refs
# base should not have changed if target is a Mercurial named
# branch (TODO one day we'll have MRs between topics)
head_commit_sha: real_source_sha,
base_commit_sha: old_diff_refs.base_sha,
start_commit_sha: old_diff_refs.start_sha,
new_diff_refs = merge_request.diff_refs
old_diff_refs: old_diff_refs,
new_diff_refs: new_diff_refs,
current_user: @current_user)
......@@ -5,7 +5,7 @@ class PostReceive
def perform(gl_repository, identifier, changes, push_options = {})
def perform(gl_repository, identifier, hg_prunes, changes, push_options = {})
project, repo_type = Gitlab::GlRepository.parse(gl_repository)
if project.nil?
......@@ -19,6 +19,10 @@ def perform(gl_repository, identifier, changes, push_options = {}) "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS']
post_received =, identifier, changes, push_options)
if repo_type.project? && project.mercurial? && !hg_prunes.nil?
hg_process_project_prunes(post_received, hg_prunes)
elsif repo_type.project?
......@@ -36,6 +40,79 @@ def identify_user(post_received)
def hg_process_project_prunes(post_received, prunes)
project = post_received.project
user = identify_user(post_received)
logprefix = "PostReceive.hg_process_project_prunes for Project id=#{}"
prunes = JSON.parse(prunes)
published_topics = prunes["topic_published"]
unless published_topics.nil?
published_topics.each_pair do |gl_branch, latest_sha|
gl_branch = base64_to_utf8(gl_branch)"#{logprefix} looking for MRs from published topic '#{gl_branch}'")
hg_branch, _topic = Gitlab::Mercurial::parse_gitlab_branch_for_hg(gl_branch)
# locate merge request with topic as source.
# The odds are that generally, a MR for a published topic will target the
# corresponding named branch. Therefore is most cases, we won't need to
# preload the MR diffs like GitLab's standard detection does: we'll reach the
# conclusion without them. Only if the MR is merged will we need the diff,
# for potential last update to `latest_sha`.
# TODO OPTIM make just one SQL request for all published topics
# and then, sort/match them (could be defeated by the way ActiveRecord
# fetches from the DB, though).
merged = project.merge_requests.opened
.where(source_branch: gl_branch, source_project: project).to_a
.select do |merge_request|
if merge_request.target_branch == 'branch/' + hg_branch
"#{logprefix} marking MR iid=#{merge_request.iid} merged: "\
"source is the published '#{gl_branch}' "\
"and target is the corresponding named branch")
real_target_sha = merge_request.target_branch_sha
# TODO what if previous position is merged and subsequent
# changes are not (or maybe in another branch?)
merged = project.repository.ancestor?(latest_sha, real_target_sha)
"#{logprefix} analysis of MR iid=#{merge_request.iid} "\
"gives merged=#{merged}. source is the published \
" "'#{gl_branch}', with latest SHA #{latest_sha} "\
"and target #{merge_request.target_branch} is another named "\
"branch with current head SHA #{real_target_sha}")
# If merged is false, we don't need to close `merge_request`:
# MergeRequests::RefreshService will take care of that later
# with all necessary details.
merged.each do |merge_request|, user).execute(merge_request, latest_sha)
# Deserialize a binary string from Base64, then guess and convert to UTF-8
# This is the same as what happens for the `changes` param, here (base64)
# and in `Gitlab::GitPostReceive` (transcoding).
def base64_to_utf8(s)
decoded = Base64.decode64(s)
return decoded if decoded.valid_encoding?
detection = CharlockHolmes::EncodingDetector.detect(changes)
return decoded unless detection && detection[:encoding]
CharlockHolmes::Converter.convert(decoded, detection[:encoding], 'UTF-8')
def hg_fixup_default_branch(post_received)
project = post_received.project
return if !project.mercurial?
......@@ -296,6 +296,7 @@ def ee_post_receive_response_hook(response)
response.reference_counter_decreased =[:gl_repository]).decrease
PostReceive.perform_async(params[:gl_repository], params[:identifier],
params[:changes], push_options.as_json)
mr_options = push_options.get(:merge_request)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment