1module Gitlab 2 module Git 3 class OperationService 4 include Gitlab::Git::Popen 5 6 BranchUpdate = Struct.new(:newrev, :repo_created, :branch_created) do 7 alias_method :repo_created?, :repo_created 8 alias_method :branch_created?, :branch_created 9 10 def self.from_gitaly(branch_update) 11 return if branch_update.nil? 12 13 new( 14 branch_update.commit_id, 15 branch_update.repo_created, 16 branch_update.branch_created 17 ) 18 end 19 end 20 21 attr_reader :user, :repository 22 23 def initialize(user, new_repository) 24 @user = user 25 @repository = new_repository 26 end 27 28 # Whenever `start_branch_name` or `start_sha` is passed, if `branch_name` 29 # doesn't exist, it will be created from the commit pointed to by 30 # `start_branch_name` or `start_sha`. 31 # 32 # If `start_repository` is passed, and the branch doesn't exist, 33 # it would try to find the commits from it instead of current repository. 34 def with_branch(branch_name, 35 start_branch_name: nil, 36 start_sha: nil, 37 start_repository: repository, 38 force: false, 39 &block) 40 start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository) 41 42 start_branch_name = nil if start_repository.empty? 43 44 if start_branch_name.present? && !start_repository.branch_exists?(start_branch_name) 45 raise ArgumentError, "Cannot find branch '#{start_branch_name}'" 46 elsif start_sha.present? && !start_repository.commit_id(start_sha) 47 raise ArgumentError, "Cannot find commit '#{start_sha}'" 48 end 49 50 update_branch_with_hooks(branch_name, force) do 51 repository.with_repo_branch_commit( 52 start_repository, 53 start_sha.presence || start_branch_name.presence || branch_name, 54 &block 55 ) 56 end 57 end 58 59 # Yields the given block (which should return a commit) and 60 # writes it to the ref while also executing hooks for it. 61 # The ref is _always_ overwritten (nothing is taken from its 62 # previous state). 63 # 64 # Returns the generated commit. 65 # 66 # ref - The target ref path we're committing to. 67 # from_ref - The ref we're taking the HEAD commit from. 68 def commit_ref(ref, source_sha, from_ref:) 69 update_autocrlf_option 70 71 target_sha = from_ref.target 72 repository.write_ref(ref, target_sha) 73 74 # Make commit 75 newrev = yield 76 77 unless newrev 78 error = "Failed to create merge commit for source_sha #{source_sha} and" \ 79 " target_sha #{target_sha} at #{ref}" 80 81 raise Gitlab::Git::CommitError.new(error) 82 end 83 84 oldrev = from_ref.target 85 86 update_ref(ref, newrev, oldrev) 87 88 newrev 89 end 90 91 private 92 93 # Returns [newrev, should_run_after_create, should_run_after_create_branch] 94 def update_branch_with_hooks(branch_name, force) 95 update_autocrlf_option 96 97 was_empty = repository.empty? 98 99 # Make commit 100 newrev = yield 101 102 raise Gitlab::Git::CommitError.new('Failed to create commit') unless newrev 103 104 branch = repository.find_branch(branch_name) 105 oldrev = find_oldrev_from_branch(newrev, branch, force) 106 107 ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name 108 update_ref_in_hooks(ref, newrev, oldrev) 109 110 BranchUpdate.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev)) 111 end 112 113 def find_oldrev_from_branch(newrev, branch, force) 114 return Gitlab::Git::BLANK_SHA unless branch 115 116 oldrev = branch.target 117 118 return oldrev if force 119 120 merge_base = repository.merge_base(newrev, branch.target) 121 raise Gitlab::Git::Repository::InvalidRef unless merge_base 122 123 if oldrev == merge_base 124 oldrev 125 else 126 raise Gitlab::Git::CommitError.new('Branch diverged') 127 end 128 end 129 130 def update_ref_in_hooks(ref, newrev, oldrev, push_options: nil, transaction: nil) 131 with_hooks(ref, newrev, oldrev, push_options: push_options, transaction: transaction) do 132 update_ref(ref, newrev, oldrev) 133 end 134 end 135 136 def with_hooks(ref, newrev, oldrev, push_options: nil, transaction: nil) 137 Gitlab::Git::HooksService.new.execute( 138 user, 139 repository, 140 oldrev, 141 newrev, 142 ref, 143 push_options: push_options, 144 transaction: transaction 145 ) do |service| 146 yield(service) 147 end 148 end 149 150 def update_ref(ref, newrev, oldrev) 151 # We use 'git update-ref' because libgit2/rugged currently does not 152 # offer 'compare and swap' ref updates. Without compare-and-swap we can 153 # (and have!) accidentally reset the ref to an earlier state, clobbering 154 # commits. See also https://github.com/libgit2/libgit2/issues/1534. 155 command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] 156 157 output, status = popen( 158 command, 159 repository.path 160 ) do |stdin| 161 stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00") 162 end 163 164 unless status.zero? 165 Gitlab::GitLogger.error("'git update-ref' in #{repository.path}: #{output}") 166 ref_name = Gitlab::Git.branch_name(ref) || ref 167 168 raise Gitlab::Git::CommitError.new( 169 "Could not update #{ref_name}." \ 170 " Please refresh and try again." 171 ) 172 end 173 end 174 175 def update_autocrlf_option 176 repository.autocrlf = :input if repository.autocrlf != :input 177 end 178 end 179 end 180end 181