1module Gitlab 2 module Git 3 class RemoteMirror 4 attr_reader :repository, :remote_name, :ssh_auth, :only_branches_matching 5 6 # An Array of local refnames that have diverged on the remote 7 # 8 # Only populated when `keep_divergent_refs` is enabled 9 attr_reader :divergent_refs 10 11 def initialize(repository, remote_name, ssh_auth:, only_branches_matching:, keep_divergent_refs:) 12 @repository = repository 13 @remote_name = remote_name 14 @ssh_auth = ssh_auth 15 @only_branches_matching = only_branches_matching 16 @keep_divergent_refs = keep_divergent_refs 17 18 @divergent_refs = [] 19 end 20 21 def update 22 ssh_auth.setup do |env| 23 # Retrieve the remote branches first since they may take a while to load, 24 # and the local branches may have changed during this time. 25 remote_branch_list = remote_branches(env: env) 26 updated_branches = changed_refs(local_branches, remote_branch_list) 27 push_refs(default_branch_first(updated_branches.keys), env: env) 28 delete_refs(local_branches, remote_branches(env: env), env: env) 29 30 local_tags = refs_obj(repository.tags) 31 remote_tags = refs_obj(repository.remote_tags(remote_name, env: env)) 32 33 updated_tags = changed_refs(local_tags, remote_tags) 34 push_refs(updated_tags.keys, env: env) 35 delete_refs(local_tags, remote_tags, env: env) 36 end 37 end 38 39 private 40 41 def ref_matchers 42 @ref_matchers ||= only_branches_matching.map do |ref| 43 GitLab::RefMatcher.new(ref) 44 end 45 end 46 47 def local_branches 48 @local_branches ||= refs_obj( 49 repository.local_branches, 50 match_refs: true 51 ) 52 end 53 54 def remote_branches(env:) 55 @remote_branches ||= refs_obj( 56 repository.remote_branches(remote_name, env: env), 57 match_refs: true 58 ) 59 end 60 61 def refs_obj(refs, match_refs: false) 62 refs.each_with_object({}) do |ref, refs| 63 next if match_refs && !include_ref?(ref.name) 64 65 key = ref.is_a?(Gitlab::Git::Tag) ? ref.refname : ref.name 66 refs[key] = ref 67 end 68 end 69 70 def changed_refs(local_refs, remote_refs) 71 local_refs.select do |ref_name, ref| 72 remote_ref = remote_refs[ref_name] 73 74 # Ref doesn't exist on the remote, it should be created 75 next true if remote_ref.nil? 76 77 local_target = ref.dereferenced_target 78 remote_target = remote_ref.dereferenced_target 79 80 if local_target == remote_target 81 # Ref is identical on the remote, no point mirroring 82 false 83 elsif @keep_divergent_refs 84 # Mirror the ref if its remote counterpart hasn't diverged 85 if repository.ancestor?(remote_target&.id, local_target&.id) 86 true 87 else 88 Gitlab::GitLogger.info("Divergent ref `#{ref_name}` in #{repository.path} due to ancestry -- remote: #{remote_target&.id}, local: #{local_target&.id}") 89 @divergent_refs << ref.refname 90 false 91 end 92 else 93 # Attempt to overwrite whatever's on the remote; push rules and 94 # protected branches may still prevent this 95 true 96 end 97 end 98 end 99 100 # Put the default branch first so it works fine when remote mirror is empty. 101 def default_branch_first(branches) 102 return unless branches.present? 103 104 default_branch, branches = branches.partition do |branch| 105 repository.root_ref == branch 106 end 107 108 branches.unshift(*default_branch) 109 end 110 111 def push_refs(refs, env:) 112 return unless refs.present? 113 114 repository.push_remote_branches(remote_name, refs, env: env) 115 end 116 117 def delete_refs(local_refs, remote_refs, env:) 118 return if @keep_divergent_refs 119 120 refs = refs_to_delete(local_refs, remote_refs) 121 122 return unless refs.present? 123 124 repository.delete_remote_branches(remote_name, refs.keys, env: env) 125 end 126 127 def refs_to_delete(local_refs, remote_refs) 128 default_branch_id = repository.commit.id 129 130 remote_refs.select do |remote_ref_name, remote_ref| 131 next false if local_refs[remote_ref_name] # skip if branch or tag exist in local repo 132 133 remote_ref_id = remote_ref.dereferenced_target.try(:id) 134 135 repository.ancestor?(remote_ref_id, default_branch_id) 136 end 137 end 138 139 def include_ref?(ref_name) 140 return true unless ref_matchers.present? 141 142 ref_matchers.any? { |matcher| matcher.matches?(ref_name) } 143 end 144 end 145 end 146end 147