diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cd19b6f47ffa386ed818b81484b4f22a7e20238e..4fcf51fb86e2860c2838bcd8a4a2b9b7c71753ba 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -474,7 +474,6 @@ db:rollback-mysql:
   variables:
     SIZE: "1"
     SETUP_DB: "false"
-    RAILS_ENV: "development"
   script:
     - git clone https://gitlab.com/gitlab-org/gitlab-test.git
        /home/git/repositories/gitlab-org/gitlab-test.git
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 38772d06dbd9693fc34a902b1295613908432897..1d5ca68137a53c78e74a444c42e4b4609ab036d5 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -18,6 +18,28 @@ module Gitlab
       InvalidBlobName = Class.new(StandardError)
       InvalidRef = Class.new(StandardError)
 
+      class << self
+        # Unlike `new`, `create` takes the storage path, not the storage name
+        def create(storage_path, name, bare: true, symlink_hooks_to: nil)
+          repo_path = File.join(storage_path, name)
+          repo_path += '.git' unless repo_path.end_with?('.git')
+
+          FileUtils.mkdir_p(repo_path, mode: 0770)
+
+          # Equivalent to `git --git-path=#{repo_path} init [--bare]`
+          repo = Rugged::Repository.init_at(repo_path, bare)
+          repo.close
+
+          if symlink_hooks_to.present?
+            hooks_path = File.join(repo_path, 'hooks')
+            FileUtils.rm_rf(hooks_path)
+            FileUtils.ln_s(symlink_hooks_to, hooks_path)
+          end
+
+          true
+        end
+      end
+
       # Full path to repo
       attr_reader :path
 
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 0cb28732402a7f1e82ccdfb4094c541eca8e99b2..280a9abf03ef459ae788c37d927db47fe1243a81 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -73,8 +73,10 @@ module Gitlab
     #
     # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
     def add_repository(storage, name)
-      gitlab_shell_fast_execute([gitlab_shell_projects_path,
-                                 'add-project', storage, "#{name}.git"])
+      Gitlab::Git::Repository.create(storage, name, bare: true, symlink_hooks_to: gitlab_shell_hooks_path)
+    rescue => err
+      Rails.logger.error("Failed to add repository #{storage}/#{name}: #{err}")
+      false
     end
 
     # Import repository
@@ -273,7 +275,11 @@ module Gitlab
     protected
 
     def gitlab_shell_path
-      Gitlab.config.gitlab_shell.path
+      File.expand_path(Gitlab.config.gitlab_shell.path)
+    end
+
+    def gitlab_shell_hooks_path
+      File.expand_path(Gitlab.config.gitlab_shell.hooks_path)
     end
 
     def gitlab_shell_user_home
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 2345874cf10bb826cc985f314af7c3fa29d24281..cfadee0bcf51ffb5ce4438c6fd632542c17aacaa 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -94,28 +94,41 @@ describe Gitlab::Shell do
   end
 
   describe 'projects commands' do
-    let(:projects_path) { 'tmp/tests/shell-projects-test/bin/gitlab-projects' }
+    let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') }
+    let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') }
+    let(:gitlab_shell_hooks_path) { File.join(gitlab_shell_path, 'hooks') }
 
     before do
-      allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-projects-test')
+      allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path)
+      allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path)
       allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800)
     end
 
     describe '#add_repository' do
-      it 'returns true when the command succeeds' do
-        expect(Gitlab::Popen).to receive(:popen)
-          .with([projects_path, 'add-project', 'current/storage', 'project/path.git'],
-                nil, popen_vars).and_return([nil, 0])
+      it 'creates a repository' do
+        created_path = File.join(TestEnv.repos_path, 'project', 'path.git')
+        hooks_path = File.join(created_path, 'hooks')
+
+        begin
+          result = gitlab_shell.add_repository(TestEnv.repos_path, 'project/path')
+
+          repo_stat = File.stat(created_path) rescue nil
+          hooks_stat = File.lstat(hooks_path) rescue nil
+          hooks_dir = File.realpath(hooks_path)
+        ensure
+          FileUtils.rm_rf(created_path)
+        end
 
-        expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be true
+        expect(result).to be_truthy
+        expect(repo_stat.mode & 0o777).to eq(0o770)
+        expect(hooks_stat.symlink?).to be_truthy
+        expect(hooks_dir).to eq(gitlab_shell_hooks_path)
       end
 
       it 'returns false when the command fails' do
-        expect(Gitlab::Popen).to receive(:popen)
-          .with([projects_path, 'add-project', 'current/storage', 'project/path.git'],
-                nil, popen_vars).and_return(["error", 1])
+        expect(FileUtils).to receive(:mkdir_p).and_raise(Errno::EEXIST)
 
-        expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be false
+        expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be_falsy
       end
     end