1# frozen_string_literal: true 2 3module MergeRequests 4 class CleanupRefsService 5 include BaseServiceUtility 6 7 TIME_THRESHOLD = 14.days 8 9 attr_reader :merge_request 10 11 def self.schedule(merge_request) 12 merge_request.create_cleanup_schedule(scheduled_at: TIME_THRESHOLD.from_now) 13 end 14 15 def initialize(merge_request) 16 @merge_request = merge_request 17 @repository = merge_request.project.repository 18 @ref_path = merge_request.ref_path 19 @merge_ref_path = merge_request.merge_ref_path 20 @ref_head_sha = @repository.commit(merge_request.ref_path)&.id 21 @merge_ref_sha = merge_request.merge_ref_head&.id 22 end 23 24 def execute 25 return error("Merge request is not scheduled to be cleaned up yet.") unless scheduled? 26 return error("Merge request has not been closed nor merged for #{TIME_THRESHOLD.inspect}.") unless eligible? 27 28 # Ensure that commit shas of refs are kept around so we won't lose them when GC runs. 29 keep_around 30 31 return error('Failed to create keep around refs.') unless kept_around? 32 return error('Failed to cache merge ref sha.') unless cache_merge_ref_sha 33 34 delete_refs if repository.exists? 35 36 return error('Failed to update schedule.') unless update_schedule 37 38 success 39 rescue Gitlab::Git::Repository::GitError, Gitlab::Git::CommandError => e 40 error(e.message) 41 end 42 43 private 44 45 attr_reader :repository, :ref_path, :merge_ref_path, :ref_head_sha, :merge_ref_sha 46 47 def scheduled? 48 merge_request.cleanup_schedule.present? && merge_request.cleanup_schedule.scheduled_at <= Time.current 49 end 50 51 def eligible? 52 return met_time_threshold?(merge_request.metrics&.latest_closed_at) if merge_request.closed? 53 54 merge_request.merged? && met_time_threshold?(merge_request.metrics&.merged_at) 55 end 56 57 def met_time_threshold?(attr) 58 attr.nil? || attr.to_i <= TIME_THRESHOLD.ago.to_i 59 end 60 61 def kept_around? 62 service = Gitlab::Git::KeepAround.new(repository) 63 64 [ref_head_sha, merge_ref_sha].compact.all? do |sha| 65 service.kept_around?(sha) 66 end 67 end 68 69 def keep_around 70 repository.keep_around(ref_head_sha, merge_ref_sha) 71 end 72 73 def cache_merge_ref_sha 74 return true if merge_ref_sha.nil? 75 76 # Caching the merge ref sha is needed before we delete the merge ref so 77 # we can still show the merge ref diff (via `MergeRequest#merge_ref_head`) 78 merge_request.update_column(:merge_ref_sha, merge_ref_sha) 79 end 80 81 def delete_refs 82 repository.delete_refs(ref_path, merge_ref_path) 83 end 84 85 def update_schedule 86 merge_request.cleanup_schedule.update(completed_at: Time.current) 87 end 88 end 89end 90