diff --git a/app/models/repository.rb b/app/models/repository.rb index 6bfa24f632910616ea150efe8002d81c97dd25c5..491d2ab69b280abe18fd62d32e6963587e00ab9f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -5,7 +5,7 @@ class Repository attr_accessor :path_with_namespace, :project - class CommitError < StandardError; end + CommitError = Class.new(StandardError) # Methods that cache data from the Git repository. # @@ -64,10 +64,6 @@ class Repository @raw_repository ||= Gitlab::Git::Repository.new(path_to_repo) end - def update_autocrlf_option - raw_repository.autocrlf = :input if raw_repository.autocrlf != :input - end - # Return absolute path to repository def path_to_repo @path_to_repo ||= File.expand_path( @@ -172,54 +168,39 @@ class Repository tags.find { |tag| tag.name == name } end - def add_branch(user, branch_name, target) - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - target = commit(target).try(:id) + def add_branch(user, branch_name, ref) + newrev = commit(ref).try(:sha) - return false unless target + return false unless newrev - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - update_ref!(ref, target, oldrev) - end + GitOperationService.new(user, self).add_branch(branch_name, newrev) after_create_branch find_branch(branch_name) end def add_tag(user, tag_name, target, message = nil) - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::TAG_REF_PREFIX + tag_name - target = commit(target).try(:id) - - return false unless target - + newrev = commit(target).try(:id) options = { message: message, tagger: user_to_committer(user) } if message - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service| - raw_tag = rugged.tags.create(tag_name, target, options) - service.newrev = raw_tag.target_id - end + return false unless newrev + + GitOperationService.new(user, self).add_tag(tag_name, newrev, options) find_tag(tag_name) end def rm_branch(user, branch_name) before_remove_branch - branch = find_branch(branch_name) - oldrev = branch.try(:dereferenced_target).try(:id) - newrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do - update_ref!(ref, newrev, oldrev) - end + GitOperationService.new(user, self).rm_branch(branch) after_remove_branch true end + # TODO: why we don't pass user here? def rm_tag(tag_name) before_remove_tag @@ -245,21 +226,6 @@ class Repository false end - def update_ref!(name, 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, path_to_repo) do |stdin| - stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") - end - - return if status.zero? - - raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.") - end - # Makes sure a commit is kept around when Git garbage collection runs. # Git GC will delete commits from the repository that are no longer in any # branches or tags, but we want to keep some of these commits around, for @@ -783,8 +749,7 @@ class Repository user, path, message, branch, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, source_project: source_project) do |ref| @@ -808,8 +773,7 @@ class Repository user, path, content, message, branch, update, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, source_project: source_project) do |ref| @@ -839,8 +803,7 @@ class Repository branch:, previous_path:, message:, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, source_project: source_project) do |ref| @@ -874,8 +837,7 @@ class Repository user, path, message, branch, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, source_project: source_project) do |ref| @@ -902,8 +864,7 @@ class Repository user:, branch:, message:, actions:, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, source_project: source_project) do |ref| @@ -964,7 +925,7 @@ class Repository end def user_to_committer(user) - Gitlab::Git::committer_hash(email: user.email, name: user.name) + Gitlab::Git.committer_hash(email: user.email, name: user.name) end def can_be_merged?(source_sha, target_branch) @@ -988,7 +949,8 @@ class Repository merge_index = rugged.merge_commits(our_commit, their_commit) return false if merge_index.conflicts? - update_branch_with_hooks(user, merge_request.target_branch) do + GitOperationService.new(user, self).with_branch( + merge_request.target_branch) do actual_options = options.merge( parents: [our_commit, their_commit], tree: merge_index.write_tree(rugged), @@ -1005,8 +967,7 @@ class Repository return false unless revert_tree_id - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( base_branch, source_commit: commit) do @@ -1027,8 +988,7 @@ class Repository return false unless cherry_pick_tree_id - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( base_branch, source_commit: commit) do @@ -1048,8 +1008,8 @@ class Repository end end - def resolve_conflicts(user, branch, params) - update_branch_with_hooks(user, branch) do + def resolve_conflicts(user, branch_name, params) + GitOperationService.new(user, self).with_branch(branch_name) do committer = user_to_committer(user) Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer)) @@ -1140,51 +1100,6 @@ class Repository fetch_ref(path_to_repo, ref, ref_path) end - # Whenever `source_branch` or `source_commit` is passed, if `branch` - # doesn't exist, it would be created from `source_branch` or - # `source_commit`. Should only pass one of them, not both. - # If `source_project` is passed, and the branch doesn't exist, - # it would try to find the source from it instead of current repository. - def update_branch_with_hooks( - current_user, branch, - source_branch: nil, source_commit: nil, source_project: project) - update_autocrlf_option - - target_branch, new_branch_added = - raw_ensure_branch( - branch, - source_branch: source_branch, - source_commit: source_commit, - source_project: source_project - ) - - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch - was_empty = empty? - - # Make commit - newrev = yield(ref) - - unless newrev - raise CommitError.new('Failed to create commit') - end - - if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil? - oldrev = Gitlab::Git::BLANK_SHA - else - oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha) - end - - GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do - update_ref!(ref, newrev, oldrev) - - # If repo was empty expire cache - after_create if was_empty - after_create_branch if was_empty || new_branch_added - end - - newrev - end - def ls_files(ref) actual_ref = ref || root_ref raw_repository.ls_files(actual_ref) @@ -1266,47 +1181,4 @@ class Repository def repository_event(event, tags = {}) Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) end - - def raw_ensure_branch( - branch_name, source_commit: nil, source_branch: nil, source_project: nil) - old_branch = find_branch(branch_name) - - if source_commit && source_branch - raise ArgumentError, - 'Should only pass either :source_branch or :source_commit, not both' - end - - if old_branch - [old_branch, false] - elsif project != source_project - unless source_branch - raise ArgumentError, - 'Should also pass :source_branch if' + - ' :source_project is different from current project' - end - - fetch_ref( - source_project.repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch}", - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch_name}" - ) - - [find_branch(branch_name), true] - elsif source_commit || source_branch - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - target = (source_commit || commit(source_branch)).try(:sha) - - unless target - raise CommitError.new( - "Cannot find branch #{branch_name} nor #{source_commit.try(:sha) || source_branch}") - end - - update_ref!(ref, target, oldrev) - - [find_branch(branch_name), true] - else - [nil, true] # Empty branch - end - end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..88175c6931d020c1bd29cd045efb7c3ee05b4b17 --- /dev/null +++ b/app/services/git_operation_service.rb @@ -0,0 +1,168 @@ + +GitOperationService = Struct.new(:user, :repository) do + def add_branch(branch_name, newrev) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + oldrev = Gitlab::Git::BLANK_SHA + + with_hooks_and_update_ref(ref, oldrev, newrev) + end + + def rm_branch(branch) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name + oldrev = branch.dereferenced_target.id + newrev = Gitlab::Git::BLANK_SHA + + with_hooks_and_update_ref(ref, oldrev, newrev) + end + + def add_tag(tag_name, newrev, options = {}) + ref = Gitlab::Git::TAG_REF_PREFIX + tag_name + oldrev = Gitlab::Git::BLANK_SHA + + with_hooks(ref, oldrev, newrev) do |service| + raw_tag = repository.rugged.tags.create(tag_name, newrev, options) + service.newrev = raw_tag.target_id + end + end + + # Whenever `source_branch` or `source_commit` is passed, if `branch` + # doesn't exist, it would be created from `source_branch` or + # `source_commit`. Should only pass one of them, not both. + # If `source_project` is passed, and the branch doesn't exist, + # it would try to find the source from it instead of current repository. + def with_branch( + branch_name, + source_branch: nil, + source_commit: nil, + source_project: repository.project) + + if source_commit && source_branch + raise ArgumentError, 'Should pass only :source_branch or :source_commit' + end + + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + oldrev = Gitlab::Git::BLANK_SHA + + if repository.branch_exists?(branch_name) + oldrev = newrev = repository.commit(branch_name).sha + + elsif repository.project != source_project + unless source_branch + raise ArgumentError, + 'Should also pass :source_branch if' + + ' :source_project is different from current project' + end + + newrev = source_project.repository.commit(source_branch).try(:sha) + + unless newrev + raise Repository::CommitError.new( + "Cannot find branch #{branch_name} nor" \ + " #{source_branch} from" \ + " #{source_project.path_with_namespace}") + end + + elsif source_commit || source_branch + newrev = (source_commit || repository.commit(source_branch)).try(:sha) + + unless newrev + raise Repository::CommitError.new( + "Cannot find branch #{branch_name} nor" \ + " #{source_commit.try(:sha) || source_branch} from" \ + " #{repository.project.path_with_namespace}") + end + + else # we want an orphan empty branch + newrev = Gitlab::Git::BLANK_SHA + end + + commit_with_hooks(ref, oldrev, newrev) do + if repository.project != source_project + repository.fetch_ref( + source_project.repository.path_to_repo, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch}", + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch_name}" + ) + end + + yield(ref) + end + end + + private + + def commit_with_hooks(ref, oldrev, newrev) + with_hooks_and_update_ref(ref, oldrev, newrev) do |service| + was_empty = repository.empty? + + # Make commit + nextrev = yield(ref) + + unless nextrev + raise Repository::CommitError.new('Failed to create commit') + end + + service.newrev = nextrev + + update_ref!(ref, nextrev, newrev) + + # If repo was empty expire cache + repository.after_create if was_empty + repository.after_create_branch if was_empty || + oldrev == Gitlab::Git::BLANK_SHA + + nextrev + end + end + + def with_hooks_and_update_ref(ref, oldrev, newrev) + with_hooks(ref, oldrev, newrev) do |service| + update_ref!(ref, newrev, oldrev) + + yield(service) if block_given? + end + end + + def with_hooks(ref, oldrev, newrev) + update_autocrlf_option + + result = nil + + GitHooksService.new.execute( + user, + repository.path_to_repo, + oldrev, + newrev, + ref) do |service| + + result = yield(service) if block_given? + end + + result + end + + def update_ref!(name, 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 #{name}\x00#{newrev}\x00#{oldrev}\x00") + end + + unless status.zero? + raise Repository::CommitError.new( + "Could not update branch #{name.sub('refs/heads/', '')}." \ + " 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 diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b797d19161d1ec323d72a6de6f8c8ea79058466d..3ce3c4dec2a87acb7737e422dae51c8e1db0c616 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -667,7 +667,7 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - repository.rm_branch(user, 'new_feature') + repository.rm_branch(user, 'feature') end.to raise_error(GitHooksService::PreReceiveError) end @@ -682,7 +682,7 @@ describe Repository, models: true do end end - describe '#update_branch_with_hooks' do + xdescribe '#update_branch_with_hooks' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev @@ -848,7 +848,7 @@ describe Repository, models: true do end it 'sets autocrlf to :input' do - repository.update_autocrlf_option + GitOperationService.new(nil, repository).send(:update_autocrlf_option) expect(repository.raw_repository.autocrlf).to eq(:input) end @@ -863,7 +863,7 @@ describe Repository, models: true do expect(repository.raw_repository).not_to receive(:autocrlf=). with(:input) - repository.update_autocrlf_option + GitOperationService.new(nil, repository).send(:update_autocrlf_option) end end end @@ -1429,14 +1429,14 @@ describe Repository, models: true do describe '#update_ref!' do it 'can create a ref' do - repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + GitOperationService.new(nil, repository).send(:update_ref!, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) expect(repository.find_branch('foobar')).not_to be_nil end it 'raises CommitError when the ref update fails' do expect do - repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + GitOperationService.new(nil, repository).send(:update_ref!, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) end.to raise_error(Repository::CommitError) end end