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