class GitOperationService attr_reader :user, :repository def initialize(new_user, new_repository) @user = new_user @repository = new_repository end def add_branch(branch_name, newrev) ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name oldrev = Gitlab::Git::BLANK_SHA update_ref_in_hooks(ref, newrev, oldrev) end def rm_branch(branch) ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name oldrev = branch.target newrev = Gitlab::Git::BLANK_SHA update_ref_in_hooks(ref, newrev, oldrev) end def add_tag(tag_name, newrev, options = {}) ref = Gitlab::Git::TAG_REF_PREFIX + tag_name oldrev = Gitlab::Git::BLANK_SHA with_hooks(ref, newrev, oldrev) do |service| # We want to pass the OID of the tag object to the hooks. For an # annotated tag we don't know that OID until after the tag object # (raw_tag) is created in the repository. That is why we have to # update the value after creating the tag object. Only the # "post-receive" hook will receive the correct value in this case. raw_tag = repository.rugged.tags.create(tag_name, newrev, options) service.newrev = raw_tag.target_id end end def rm_tag(tag) ref = Gitlab::Git::TAG_REF_PREFIX + tag.name oldrev = tag.target newrev = Gitlab::Git::BLANK_SHA update_ref_in_hooks(ref, newrev, oldrev) do repository.rugged.tags.delete(tag_name) end end # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist, # it would be created from `start_branch_name`. # If `start_project` is passed, and the branch doesn't exist, # it would try to find the commits from it instead of current repository. def with_branch( branch_name, start_branch_name: nil, start_project: repository.project, &block) start_repository = start_project.repository start_branch_name = nil if start_repository.empty_repo? if start_branch_name && !start_repository.branch_exists?(start_branch_name) raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.path_with_namespace}" end update_branch_with_hooks(branch_name) do repository.with_repo_branch_commit( start_repository, start_branch_name || branch_name, &block) end end private def update_branch_with_hooks(branch_name) update_autocrlf_option was_empty = repository.empty? # Make commit newrev = yield unless newrev raise Repository::CommitError.new('Failed to create commit') end branch = repository.find_branch(branch_name) oldrev = find_oldrev_from_branch(newrev, branch) ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name update_ref_in_hooks(ref, newrev, oldrev) # If repo was empty expire cache repository.after_create if was_empty repository.after_create_branch if was_empty || Gitlab::Git.blank_ref?(oldrev) newrev end def find_oldrev_from_branch(newrev, branch) return Gitlab::Git::BLANK_SHA unless branch oldrev = branch.target if oldrev == repository.rugged.merge_base(newrev, branch.target) oldrev else raise Repository::CommitError.new('Branch diverged') end end def update_ref_in_hooks(ref, newrev, oldrev) with_hooks(ref, newrev, oldrev) do update_ref(ref, newrev, oldrev) end end def with_hooks(ref, newrev, oldrev) GitHooksService.new.execute( user, repository.path_to_repo, oldrev, newrev, ref) do |service| yield(service) end end def update_ref(ref, newrev, oldrev) # We use 'git update-ref' because libgit2/rugged currently does not # offer 'compare and swap' ref updates. Without compare-and-swap we can # (and have!) accidentally reset the ref to an earlier state, clobbering # commits. See also https://github.com/libgit2/libgit2/issues/1534. command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] _, status = Gitlab::Popen.popen( command, repository.path_to_repo) do |stdin| stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00") end unless status.zero? raise Repository::CommitError.new( "Could not update branch #{Gitlab::Git.branch_name(ref)}." \ " Please refresh and try again.") end end def update_autocrlf_option if repository.raw_repository.autocrlf != :input repository.raw_repository.autocrlf = :input end end end