summaryrefslogtreecommitdiff
path: root/app/services/git
diff options
context:
space:
mode:
Diffstat (limited to 'app/services/git')
-rw-r--r--app/services/git/base_hooks_service.rb105
-rw-r--r--app/services/git/branch_hooks_service.rb144
-rw-r--r--app/services/git/branch_push_service.rb92
-rw-r--r--app/services/git/tag_hooks_service.rb36
-rw-r--r--app/services/git/tag_push_service.rb14
5 files changed, 391 insertions, 0 deletions
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
new file mode 100644
index 00000000000..d30df34e54b
--- /dev/null
+++ b/app/services/git/base_hooks_service.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Git
+ class BaseHooksService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ # The N most recent commits to process in a single push payload.
+ PROCESS_COMMIT_LIMIT = 100
+
+ def execute
+ project.repository.after_create if project.empty_repo?
+
+ create_events
+ create_pipelines
+ execute_project_hooks
+
+ # Not a hook, but it needs access to the list of changed commits
+ enqueue_invalidate_cache
+
+ update_remote_mirrors
+
+ push_data
+ end
+
+ private
+
+ def hook_name
+ raise NotImplementedError, "Please implement #{self.class}##{__method__}"
+ end
+
+ def commits
+ raise NotImplementedError, "Please implement #{self.class}##{__method__}"
+ end
+
+ def limited_commits
+ commits.last(PROCESS_COMMIT_LIMIT)
+ end
+
+ def commits_count
+ commits.count
+ end
+
+ def event_message
+ nil
+ end
+
+ def invalidated_file_types
+ []
+ end
+
+ def create_events
+ EventCreateService.new.push(project, current_user, push_data)
+ end
+
+ def create_pipelines
+ return unless params.fetch(:create_pipelines, true)
+
+ Ci::CreatePipelineService
+ .new(project, current_user, push_data)
+ .execute(:push, pipeline_options)
+ end
+
+ def execute_project_hooks
+ project.execute_hooks(push_data, hook_name)
+ project.execute_services(push_data, hook_name)
+ end
+
+ def enqueue_invalidate_cache
+ ProjectCacheWorker.perform_async(
+ project.id,
+ invalidated_file_types,
+ [:commit_count, :repository_size]
+ )
+ end
+
+ def push_data
+ @push_data ||= Gitlab::DataBuilder::Push.build(
+ project: project,
+ user: current_user,
+ oldrev: params[:oldrev],
+ newrev: params[:newrev],
+ ref: params[:ref],
+ commits: limited_commits,
+ message: event_message,
+ commits_count: commits_count,
+ push_options: params[:push_options] || {}
+ )
+
+ # Dependent code may modify the push data, so return a duplicate each time
+ @push_data.dup
+ end
+
+ # to be overridden in EE
+ def pipeline_options
+ {}
+ end
+
+ def update_remote_mirrors
+ return unless project.has_remote_mirror?
+
+ project.mark_stuck_remote_mirrors_as_failed!
+ project.update_remote_mirrors
+ end
+ end
+end
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
new file mode 100644
index 00000000000..d21a6bb1b9a
--- /dev/null
+++ b/app/services/git/branch_hooks_service.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+module Git
+ class BranchHooksService < ::Git::BaseHooksService
+ def execute
+ execute_branch_hooks
+
+ super.tap do
+ enqueue_update_gpg_signatures
+ end
+ end
+
+ private
+
+ def hook_name
+ :push_hooks
+ end
+
+ def commits
+ strong_memoize(:commits) do
+ if creating_default_branch?
+ # The most recent PROCESS_COMMIT_LIMIT commits in the default branch
+ offset = [count_commits_in_branch - PROCESS_COMMIT_LIMIT, 0].max
+ project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
+ elsif creating_branch?
+ # 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.
+ project.repository.commits_between(project.default_branch, params[:newrev])
+ elsif updating_branch?
+ project.repository.commits_between(params[:oldrev], params[:newrev])
+ else # removing branch
+ []
+ end
+ end
+ end
+
+ def commits_count
+ return count_commits_in_branch if creating_default_branch?
+
+ super
+ end
+
+ def invalidated_file_types
+ return super unless default_branch? && !creating_branch?
+
+ paths = limited_commits.each_with_object(Set.new) do |commit, set|
+ commit.raw_deltas.each do |diff|
+ set << diff.new_path
+ end
+ end
+
+ Gitlab::FileDetector.types_in_paths(paths)
+ end
+
+ def execute_branch_hooks
+ project.repository.after_push_commit(branch_name)
+
+ branch_create_hooks if creating_branch?
+ branch_update_hooks if updating_branch?
+ branch_change_hooks if creating_branch? || updating_branch?
+ branch_remove_hooks if removing_branch?
+ end
+
+ def branch_create_hooks
+ project.repository.after_create_branch
+ project.after_create_default_branch if default_branch?
+ end
+
+ def branch_update_hooks
+ # Update the bare repositories info/attributes file using the contents of
+ # the default branch's .gitattributes file
+ project.repository.copy_gitattributes(params[:ref]) if default_branch?
+ end
+
+ def branch_change_hooks
+ enqueue_process_commit_messages
+ end
+
+ def branch_remove_hooks
+ project.repository.after_remove_branch
+ end
+
+ # Schedules processing of commit messages
+ def enqueue_process_commit_messages
+ # don't process commits for the initial push to the default branch
+ return if creating_default_branch?
+
+ limited_commits.each do |commit|
+ next unless commit.matches_cross_reference_regex?
+
+ ProcessCommitWorker.perform_async(
+ project.id,
+ current_user.id,
+ commit.to_hash,
+ default_branch?
+ )
+ end
+ end
+
+ def enqueue_update_gpg_signatures
+ unsigned = GpgSignature.unsigned_commit_shas(limited_commits.map(&:sha))
+ return if unsigned.empty?
+
+ signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned)
+ return if signable.empty?
+
+ CreateGpgSignatureWorker.perform_async(signable, project.id)
+ end
+
+ def creating_branch?
+ Gitlab::Git.blank_ref?(params[:oldrev])
+ end
+
+ def updating_branch?
+ !creating_branch? && !removing_branch?
+ end
+
+ def removing_branch?
+ Gitlab::Git.blank_ref?(params[:newrev])
+ end
+
+ def creating_default_branch?
+ creating_branch? && default_branch?
+ end
+
+ def count_commits_in_branch
+ strong_memoize(:count_commits_in_branch) do
+ project.repository.commit_count_for_ref(params[:ref])
+ end
+ end
+
+ def default_branch?
+ strong_memoize(:default_branch) do
+ [nil, branch_name].include?(project.default_branch)
+ end
+ end
+
+ def branch_name
+ strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) }
+ end
+ end
+end
diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb
new file mode 100644
index 00000000000..c4910180787
--- /dev/null
+++ b/app/services/git/branch_push_service.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+module Git
+ class BranchPushService < ::BaseService
+ include Gitlab::Access
+ include Gitlab::Utils::StrongMemoize
+
+ # 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
+ return unless Gitlab::Git.branch_ref?(params[:ref])
+
+ enqueue_update_mrs
+ enqueue_detect_repository_languages
+
+ execute_related_hooks
+ perform_housekeeping
+
+ stop_environments
+
+ true
+ end
+
+ # Update merge requests that may be affected by this push. A new branch
+ # could cause the last commit of a merge request to change.
+ def enqueue_update_mrs
+ UpdateMergeRequestsWorker.perform_async(
+ project.id,
+ current_user.id,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref]
+ )
+ end
+
+ def enqueue_detect_repository_languages
+ return unless default_branch?
+
+ DetectRepositoryLanguagesWorker.perform_async(project.id)
+ end
+
+ # Only stop environments if the ref is a branch that is being deleted
+ def stop_environments
+ return unless removing_branch?
+
+ Ci::StopEnvironmentsService.new(project, current_user).execute(branch_name)
+ end
+
+ 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
+ BranchHooksService.new(project, current_user, params).execute
+ end
+
+ def perform_housekeeping
+ housekeeping = Projects::HousekeepingService.new(project)
+ housekeeping.increment!
+ housekeeping.execute if housekeeping.needed?
+ rescue Projects::HousekeepingService::LeaseTaken
+ end
+
+ def removing_branch?
+ Gitlab::Git.blank_ref?(params[:newrev])
+ end
+
+ def branch_name
+ strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) }
+ end
+
+ def default_branch?
+ strong_memoize(:default_branch) do
+ [nil, branch_name].include?(project.default_branch)
+ end
+ end
+ end
+end
diff --git a/app/services/git/tag_hooks_service.rb b/app/services/git/tag_hooks_service.rb
new file mode 100644
index 00000000000..18eb780579f
--- /dev/null
+++ b/app/services/git/tag_hooks_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Git
+ class TagHooksService < ::Git::BaseHooksService
+ private
+
+ def hook_name
+ :tag_push_hooks
+ end
+
+ def commits
+ [tag_commit].compact
+ end
+
+ def event_message
+ tag&.message
+ end
+
+ def tag
+ strong_memoize(:tag) do
+ next if Gitlab::Git.blank_ref?(params[:newrev])
+
+ tag_name = Gitlab::Git.ref_name(params[:ref])
+ tag = project.repository.find_tag(tag_name)
+
+ tag if tag && tag.target == params[:newrev]
+ end
+ end
+
+ def tag_commit
+ strong_memoize(:tag_commit) do
+ project.commit(tag.dereferenced_target) if tag
+ end
+ end
+ end
+end
diff --git a/app/services/git/tag_push_service.rb b/app/services/git/tag_push_service.rb
new file mode 100644
index 00000000000..ee4166dccd0
--- /dev/null
+++ b/app/services/git/tag_push_service.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Git
+ class TagPushService < ::BaseService
+ def execute
+ return unless Gitlab::Git.tag_ref?(params[:ref])
+
+ project.repository.before_push_tag
+ TagHooksService.new(project, current_user, params).execute
+
+ true
+ end
+ end
+end