gitlab-org--gitlab-foss/app/services/git_push_service.rb
Jonathon Reinhart ba781484c7 Add support for Git push options, specifically ci.skip
gitlab-org/gitlab-shell!166 added support for collecting push options
from the environment, and passing them along to the
/internal/post_receive API endpoint.

This change handles the new push_options JSON element in the payload,
and passes them on through to the GitPushService and GitTagPushService
services.

Futhermore, it adds support for the first push option, ci.skip.  With
this change, one can use 'git push -o ci.skip' to skip CI pipe
execution. Note that the pipeline is still created, but in the "skipped"
state, just like with the 'ci skip' commit message text.

Implements #18667
2018-12-31 13:57:53 -05:00

234 lines
6.7 KiB
Ruby

# frozen_string_literal: true
class GitPushService < BaseService
attr_accessor :push_data, :push_commits
include Gitlab::Access
include Gitlab::Utils::StrongMemoize
# The N most recent commits to process in a single push payload.
PROCESS_COMMIT_LIMIT = 100
# This method will be called after each git update
# and only if the provided user and project are present in GitLab.
#
# All callbacks for post receive action should be placed here.
#
# Next, this method:
# 1. Creates the push event
# 2. Updates merge requests
# 3. Recognizes cross-references from commit messages
# 4. Executes the project's webhooks
# 5. Executes the project's services
# 6. Checks if the project's main language has changed
#
def execute
project.repository.after_create if project.empty_repo?
project.repository.after_push_commit(branch_name)
if push_remove_branch?
project.repository.after_remove_branch
@push_commits = []
elsif push_to_new_branch?
project.repository.after_create_branch
# Re-find the pushed commits.
if default_branch?
# Initial push to the default branch. Take the full history of that branch as "newly pushed".
process_default_branch
else
# Use the pushed commits that aren't reachable by the default branch
# as a heuristic. This may include more commits than are actually pushed, but
# that shouldn't matter because we check for existing cross-references later.
@push_commits = project.repository.commits_between(project.default_branch, params[:newrev])
# don't process commits for the initial push to the default branch
process_commit_messages
end
elsif push_to_existing_branch?
# Collect data for this git push
@push_commits = project.repository.commits_between(params[:oldrev], params[:newrev])
process_commit_messages
# Update the bare repositories info/attributes file using the contents of the default branches
# .gitattributes file
update_gitattributes if default_branch?
end
execute_related_hooks
perform_housekeeping
update_remote_mirrors
update_caches
update_signatures
end
def update_gitattributes
project.repository.copy_gitattributes(params[:ref])
end
def update_caches
if default_branch?
if push_to_new_branch?
# If this is the initial push into the default branch, the file type caches
# will already be reset as a result of `Project#change_head`.
types = []
else
paths = Set.new
last_pushed_commits.each do |commit|
commit.raw_deltas.each do |diff|
paths << diff.new_path
end
end
types = Gitlab::FileDetector.types_in_paths(paths.to_a)
end
DetectRepositoryLanguagesWorker.perform_async(@project.id, current_user.id)
else
types = []
end
ProjectCacheWorker.perform_async(project.id, types, [:commit_count, :repository_size])
end
# rubocop: disable CodeReuse/ActiveRecord
def update_signatures
commit_shas = last_pushed_commits.map(&:sha)
return if commit_shas.empty?
shas_with_cached_signatures = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha)
commit_shas -= shas_with_cached_signatures
return if commit_shas.empty?
commit_shas = Gitlab::Git::Commit.shas_with_signatures(project.repository, commit_shas)
CreateGpgSignatureWorker.perform_async(commit_shas, project.id)
end
# rubocop: enable CodeReuse/ActiveRecord
# Schedules processing of commit messages.
def process_commit_messages
default = default_branch?
last_pushed_commits.each do |commit|
if commit.matches_cross_reference_regex?
ProcessCommitWorker
.perform_async(project.id, current_user.id, commit.to_hash, default)
end
end
end
protected
def update_remote_mirrors
return unless project.has_remote_mirror?
project.mark_stuck_remote_mirrors_as_failed!
project.update_remote_mirrors
end
def execute_related_hooks
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
#
UpdateMergeRequestsWorker
.perform_async(project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
EventCreateService.new.push(project, current_user, build_push_data)
Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push)
project.execute_hooks(build_push_data.dup, :push_hooks)
project.execute_services(build_push_data.dup, :push_hooks)
if push_remove_branch?
AfterBranchDeleteService
.new(project, current_user)
.execute(branch_name)
end
end
def perform_housekeeping
housekeeping = Projects::HousekeepingService.new(project)
housekeeping.increment!
housekeeping.execute if housekeeping.needed?
rescue Projects::HousekeepingService::LeaseTaken
end
def process_default_branch
offset = [push_commits_count_for_ref - PROCESS_COMMIT_LIMIT, 0].max
@push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
project.after_create_default_branch
end
def build_push_data
@push_data ||= Gitlab::DataBuilder::Push.build(
project,
current_user,
params[:oldrev],
params[:newrev],
params[:ref],
@push_commits,
commits_count: commits_count,
push_options: params[:push_options] || [])
end
def push_to_existing_branch?
# Return if this is not a push to a branch (e.g. new commits)
branch_ref? && !Gitlab::Git.blank_ref?(params[:oldrev])
end
def push_to_new_branch?
strong_memoize(:push_to_new_branch) do
branch_ref? && Gitlab::Git.blank_ref?(params[:oldrev])
end
end
def push_remove_branch?
strong_memoize(:push_remove_branch) do
branch_ref? && Gitlab::Git.blank_ref?(params[:newrev])
end
end
def default_branch?
branch_ref? &&
(branch_name == project.default_branch || project.default_branch.nil?)
end
def commit_user(commit)
commit.author || current_user
end
def branch_name
strong_memoize(:branch_name) do
Gitlab::Git.ref_name(params[:ref])
end
end
def branch_ref?
strong_memoize(:branch_ref) do
Gitlab::Git.branch_ref?(params[:ref])
end
end
def commits_count
return push_commits_count_for_ref if default_branch? && push_to_new_branch?
Array(@push_commits).size
end
def push_commits_count_for_ref
strong_memoize(:push_commits_count_for_ref) do
project.repository.commit_count_for_ref(params[:ref])
end
end
def last_pushed_commits
@last_pushed_commits ||= @push_commits.last(PROCESS_COMMIT_LIMIT)
end
end