From ad6d6232342558705c54ba70a94f9d7ddbd00f8c Mon Sep 17 00:00:00 2001
From: Douwe Maan <douwe@gitlab.com>
Date: Tue, 17 Feb 2015 16:59:50 +0100
Subject: [PATCH] Add Bitbucket importer.

---
 CHANGELOG                                     |   2 +
 Gemfile                                       |   1 +
 Gemfile.lock                                  |   5 +
 .../images/authbuttons/bitbucket_32.png       | Bin 0 -> 2713 bytes
 .../images/authbuttons/bitbucket_64.png       | Bin 0 -> 2163 bytes
 .../import/bitbucket_controller.rb            |  74 +++++++++++++++
 app/helpers/oauth_helper.rb                   |   4 +-
 app/helpers/projects_helper.rb                |   4 +
 app/models/project.rb                         |   2 +-
 app/models/user.rb                            |   1 +
 app/views/import/base/create.js.haml          |   7 ++
 app/views/import/bitbucket/status.html.haml   |  44 +++++++++
 app/views/import/github/status.html.haml      |   2 +-
 app/views/import/gitlab/status.html.haml      |   2 +-
 .../_bitbucket_import_modal.html.haml         |   9 ++
 app/views/projects/new.html.haml              |  13 +++
 app/workers/repository_import_worker.rb       |   2 +
 config/gitlab.yml.example                     |  18 ++--
 config/routes.rb                              |   5 +
 ...tbucket_access_token_and_secret_to_user.rb |   6 ++
 db/schema.rb                                  |  40 ++++----
 doc/integration/bitbucket.m                   |   1 +
 lib/gitlab/bitbucket_import/client.rb         |  88 ++++++++++++++++++
 lib/gitlab/bitbucket_import/importer.rb       |  52 +++++++++++
 lib/gitlab/bitbucket_import/key_adder.rb      |  22 +++++
 .../bitbucket_import/project_creator.rb       |  39 ++++++++
 lib/gitlab/github_import/client.rb            |   6 +-
 lib/gitlab/github_import/importer.rb          |   2 +-
 lib/gitlab/gitlab_import/client.rb            |  10 +-
 lib/gitlab/gitlab_import/importer.rb          |   2 +-
 lib/gitlab/import_formatter.rb                |   2 +-
 .../import/bitbucket_controller_spec.rb       |  77 +++++++++++++++
 .../bitbucket_import/project_creator_spec.rb  |  22 +++++
 .../project_creator_spec.rb}                  |   7 +-
 ...ect_creator.rb => project_creator_spec.rb} |   5 +-
 35 files changed, 522 insertions(+), 54 deletions(-)
 create mode 100644 app/assets/images/authbuttons/bitbucket_32.png
 create mode 100644 app/assets/images/authbuttons/bitbucket_64.png
 create mode 100644 app/controllers/import/bitbucket_controller.rb
 create mode 100644 app/views/import/bitbucket/status.html.haml
 create mode 100644 app/views/projects/_bitbucket_import_modal.html.haml
 create mode 100644 db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb
 create mode 100644 doc/integration/bitbucket.m
 create mode 100644 lib/gitlab/bitbucket_import/client.rb
 create mode 100644 lib/gitlab/bitbucket_import/importer.rb
 create mode 100644 lib/gitlab/bitbucket_import/key_adder.rb
 create mode 100644 lib/gitlab/bitbucket_import/project_creator.rb
 create mode 100644 spec/controllers/import/bitbucket_controller_spec.rb
 create mode 100644 spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
 rename spec/lib/gitlab/{github/project_creator.rb => github_import/project_creator_spec.rb} (77%)
 rename spec/lib/gitlab/gitlab_import/{project_creator.rb => project_creator_spec.rb} (91%)

diff --git a/CHANGELOG b/CHANGELOG
index d879ee857281d..5a3a2ca2e2479 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -72,6 +72,8 @@ v 7.8.0
   - Improve database performance for GitLab
   - Add Asana service (Jeremy Benoist)
   - Improve project web hooks with extra data
+  - Add Bitbucket omniauth provider.
+  - Add Bitbucket importer.
 
 v 7.7.2
   - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch
diff --git a/Gemfile b/Gemfile
index 093f7bacc06ee..ad01b2b43e088 100644
--- a/Gemfile
+++ b/Gemfile
@@ -30,6 +30,7 @@ gem 'omniauth-github'
 gem 'omniauth-shibboleth'
 gem 'omniauth-kerberos'
 gem 'omniauth-gitlab'
+gem 'omniauth-bitbucket'
 gem 'doorkeeper', '2.1.0'
 gem "rack-oauth2", "~> 1.0.5"
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 4bc47836e7126..226591a50af8f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -338,6 +338,10 @@ GEM
     omniauth (1.1.4)
       hashie (>= 1.2, < 3)
       rack
+    omniauth-bitbucket (0.0.2)
+      multi_json (~> 1.7)
+      omniauth (~> 1.1)
+      omniauth-oauth (~> 1.0)
     omniauth-github (1.1.1)
       omniauth (~> 1.0)
       omniauth-oauth2 (~> 1.1)
@@ -701,6 +705,7 @@ DEPENDENCIES
   nprogress-rails
   octokit (= 3.7.0)
   omniauth (~> 1.1.3)
+  omniauth-bitbucket
   omniauth-github
   omniauth-gitlab
   omniauth-google-oauth2
diff --git a/app/assets/images/authbuttons/bitbucket_32.png b/app/assets/images/authbuttons/bitbucket_32.png
new file mode 100644
index 0000000000000000000000000000000000000000..27702eb973d1c3c0b9a39eee082ca72d38339cc2
GIT binary patch
literal 2713
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
Wr6+Vw|FjX*Ui5VJb6Mw<&;$U)f?<FF

literal 0
HcmV?d00001

diff --git a/app/assets/images/authbuttons/bitbucket_64.png b/app/assets/images/authbuttons/bitbucket_64.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b90a57bc7de93163275f99ea2971e2740af98af
GIT binary patch
literal 2163
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
Z2l2sC`jTr~AA_0|44$rjF6*2UngCWPwSfQt

literal 0
HcmV?d00001

diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
new file mode 100644
index 0000000000000..27e91f49f2b57
--- /dev/null
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -0,0 +1,74 @@
+class Import::BitbucketController < Import::BaseController
+  before_filter :bitbucket_auth, except: :callback
+
+  # rescue_from OAuth::Error, with: :bitbucket_unauthorized
+
+  def callback
+    request_token = session.delete(:oauth_request_token) 
+    raise "Session expired!" if request_token.nil?
+
+    request_token.symbolize_keys!
+    
+    access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url)
+
+    current_user.bitbucket_access_token = access_token.token
+    current_user.bitbucket_access_token_secret = access_token.secret
+
+    current_user.save
+    redirect_to status_import_bitbucket_url
+  end
+
+  def status
+    @repos = client.projects
+    
+    @already_added_projects = current_user.created_projects.where(import_type: "bitbucket")
+    already_added_projects_names = @already_added_projects.pluck(:import_source)
+
+    @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" }
+  end
+
+  def jobs
+    jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status])
+    render json: jobs
+  end
+
+  def create
+    @repo_id = params[:repo_id] || ""
+    repo = client.project(@repo_id.gsub("___", "/"))
+    @target_namespace = params[:new_namespace].presence || repo["owner"]
+    @project_name = repo["slug"]
+    
+    namespace = get_or_create_namespace || (render and return)
+
+    unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user).execute
+      @access_denied = true
+      render
+      return
+    end
+
+    @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user).execute
+  end
+
+  private
+
+  def client
+    @client ||= Gitlab::BitbucketImport::Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret)
+  end
+
+  def bitbucket_auth
+    if current_user.bitbucket_access_token.blank?
+      go_to_bitbucket_for_permissions
+    end
+  end
+
+  def go_to_bitbucket_for_permissions
+    request_token = client.request_token(callback_import_bitbucket_url)
+    session[:oauth_request_token] = request_token
+
+    redirect_to client.authorize_url(request_token, callback_import_bitbucket_url)
+  end
+
+  def bitbucket_unauthorized
+    go_to_bitbucket_for_permissions
+  end
+end
diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb
index c7bc9307a58c3..848d74c18c34b 100644
--- a/app/helpers/oauth_helper.rb
+++ b/app/helpers/oauth_helper.rb
@@ -4,7 +4,7 @@ def ldap_enabled?
   end
 
   def default_providers
-    [:twitter, :github, :gitlab, :google_oauth2, :ldap]
+    [:twitter, :github, :gitlab, :bitbucket, :google_oauth2, :ldap]
   end
 
   def enabled_oauth_providers
@@ -13,7 +13,7 @@ def enabled_oauth_providers
 
   def enabled_social_providers
     enabled_oauth_providers.select do |name|
-      [:twitter, :gitlab, :github, :google_oauth2].include?(name.to_sym)
+      [:twitter, :gitlab, :github, :bitbucket, :google_oauth2].include?(name.to_sym)
     end
   end
 
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 900afde4d9bdd..8a48a9d394636 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -273,4 +273,8 @@ def github_import_enabled?
   def gitlab_import_enabled?
     enabled_oauth_providers.include?(:gitlab)
   end
+
+  def bitbucket_import_enabled?
+    enabled_oauth_providers.include?(:bitbucket)
+  end
 end
diff --git a/app/models/project.rb b/app/models/project.rb
index 91ab788083d0f..d858f7f293793 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -130,7 +130,7 @@ class Project < ActiveRecord::Base
   validates_uniqueness_of :name, scope: :namespace_id
   validates_uniqueness_of :path, scope: :namespace_id
   validates :import_url,
-    format: { with: URI::regexp(%w(git http https)), message: 'should be a valid url' },
+    format: { with: URI::regexp(%w(ssh git http https)), message: 'should be a valid url' },
     if: :import?
   validates :star_count, numericality: { greater_than_or_equal_to: 0 }
   validate :check_limit, on: :create
diff --git a/app/models/user.rb b/app/models/user.rb
index 08ad619a90c57..b381ee2712017 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -46,6 +46,7 @@
 #  github_access_token      :string(255)
 #  notification_email       :string(255)
 #  password_automatically_set :boolean        default(FALSE)
+#  bitbucket_access_token   :string(255)
 #
 
 require 'carrierwave/orm/activerecord'
diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml
index cd4c9fbf360a9..8ebdf4f1a2040 100644
--- a/app/views/import/base/create.js.haml
+++ b/app/views/import/base/create.js.haml
@@ -10,9 +10,16 @@
     target_field.append("/" + project_name)
     target_field.data("project_name", project_name)
     target_field.find('input').prop("value", origin_namespace)
+- elsif @access_denied
+  :plain
+    job = $("tr#repo_#{@repo_id}")
+    job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>"")
 - else
   :plain
     job = $("tr#repo_#{@repo_id}")
     job.attr("id", "project_#{@project.id}")
+    target_field = job.find(".import-target")
+    target_field.empty()
+    target_field.append('<strong>#{link_to @project.path_with_namespace, @project}</strong>')
     $("table.import-jobs tbody").prepend(job)
     job.addClass("active").find(".import-actions").html("<i class='fa fa-spinner fa-spin'></i> started")
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
new file mode 100644
index 0000000000000..7a2613e4b03ce
--- /dev/null
+++ b/app/views/import/bitbucket/status.html.haml
@@ -0,0 +1,44 @@
+%h3.page-title
+  %i.fa.fa-bitbucket
+  Import repositories from Bitbucket
+
+%p.light
+  Select projects you want to import.
+%hr
+%p
+  = button_tag 'Import all projects', class: "btn btn-success js-import-all"
+
+%table.table.import-jobs
+  %thead
+    %tr
+      %th From Bitbucket
+      %th To GitLab
+      %th Status
+  %tbody
+    - @already_added_projects.each do |project|
+      %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+        %td= project.import_source
+        %td
+          %strong= link_to project.path_with_namespace, project
+        %td.job-status
+          - if project.import_status == 'finished'
+            %span.cgreen
+              %i.fa.fa-check
+              done
+          - elsif project.import_status == 'started'
+            %i.fa.fa-spinner.fa-spin
+            started
+          - else
+            = project.human_import_status_name
+
+    - @repos.each do |repo|
+      %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+        %td= "#{repo["owner"]}/#{repo["slug"]}"
+        %td.import-target
+          = "#{repo["owner"]}/#{repo["slug"]}"
+        %td.import-actions.job-status
+          = button_tag "Import", class: "btn js-add-to-import"
+
+:coffeescript
+  $ ->
+    new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}")
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 84d9903fe15cc..b1538b1a41e2f 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,6 +1,6 @@
 %h3.page-title
   %i.fa.fa-github
-  Import repositories from GitHub.com
+  Import repositories from GitHub
 
 %p.light
   Select projects you want to import.
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index d1e48dfad2006..43db102994fa0 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -1,5 +1,5 @@
 %h3.page-title
-  %i.fa.fa-github
+  %i.fa.fa-heart
   Import repositories from GitLab.com
 
 %p.light
diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml
new file mode 100644
index 0000000000000..dd7aacc7e6e9b
--- /dev/null
+++ b/app/views/projects/_bitbucket_import_modal.html.haml
@@ -0,0 +1,9 @@
+%div#bitbucket_import_modal.modal.hide
+  .modal-dialog
+    .modal-content
+      .modal-header
+        %a.close{href: "#", "data-dismiss" => "modal"} ×
+        %h3 GitHub OAuth import
+      .modal-body
+        You need to setup integration with Bitbucket first.
+        = link_to 'How to setup integration with Bitbucket', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/bitbucket.md'
\ No newline at end of file
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 5216f308110fa..875c092fd1a52 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -53,6 +53,19 @@
               Import projects from GitHub
             = render 'github_import_modal'
 
+      .project-import.form-group
+        .col-sm-2
+        .col-sm-10
+          - if bitbucket_import_enabled?
+            = link_to status_import_bitbucket_path do
+              %i.fa.fa-bitbucket
+              Import projects from Bitbucket
+          - else
+            = link_to '#', class: 'how_to_import_link light' do
+              %i.fa.fa-bitbucket
+              Import projects from Bitbucket
+            = render 'bitbucket_import_modal'
+      
       - unless request.host == 'gitlab.com'
         .project-import.form-group
           .col-sm-2
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 5f9970d3795a4..d7e759fb4701f 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -14,6 +14,8 @@ def perform(project_id)
                               Gitlab::GithubImport::Importer.new(project).execute
                             elsif project.import_type == 'gitlab'
                               Gitlab::GitlabImport::Importer.new(project).execute
+                            elsif project.import_type == 'bitbucket'
+                              Gitlab::BitbucketImport::Importer.new(project).execute
                             else
                               true
                             end
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 044b1f66b25ca..6dff07cf9df36 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -207,17 +207,19 @@ production: &base
     # arguments, followed by optional 'args' which can be either a hash or an array.
     # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html
     providers:
-      # - { name: 'google_oauth2', app_id: 'YOUR APP ID',
-      #     app_secret: 'YOUR APP SECRET',
+      # - { name: 'google_oauth2', app_id: 'YOUR_APP_ID',
+      #     app_secret: 'YOUR_APP_SECRET',
       #     args: { access_type: 'offline', approval_prompt: '' } }
-      # - { name: 'twitter', app_id: 'YOUR APP ID',
-      #     app_secret: 'YOUR APP SECRET'}
-      # - { name: 'github', app_id: 'YOUR APP ID',
-      #     app_secret: 'YOUR APP SECRET',
+      # - { name: 'twitter', app_id: 'YOUR_APP_ID',
+      #     app_secret: 'YOUR_APP_SECRET'}
+      # - { name: 'github', app_id: 'YOUR_APP_ID',
+      #     app_secret: 'YOUR_APP_SECRET',
       #     args: { scope: 'user:email' } }
-      # - { name: 'gitlab', app_id: 'YOUR APP ID',
-      #     app_secret: 'YOUR APP SECRET',
+      # - { name: 'gitlab', app_id: 'YOUR_APP_ID',
+      #     app_secret: 'YOUR_APP_SECRET',
       #     args: { scope: 'api' } }
+      # - { name: 'bitbucket', app_id: 'YOUR_APP_ID',
+      #     app_secret: 'YOUR_APP_SECRET'}
 
 
 
diff --git a/config/routes.rb b/config/routes.rb
index ecd439aeceab6..57964bdc3b7d2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -68,6 +68,11 @@
       get :jobs
     end
 
+    resource :bitbucket, only: [:create, :new], controller: :bitbucket do
+      get :status
+      get :callback
+      get :jobs
+    end
     resource :gitorious, only: [:create, :new], controller: :gitorious do
       get :status
       get :callback
diff --git a/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb
new file mode 100644
index 0000000000000..23ac1b399ec40
--- /dev/null
+++ b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb
@@ -0,0 +1,6 @@
+class AddBitbucketAccessTokenAndSecretToUser < ActiveRecord::Migration
+  def change
+    add_column :users, :bitbucket_access_token, :string
+    add_column :users, :bitbucket_access_token_secret, :string
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e11a068c9c559..8069d95c69872 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20150213121042) do
+ActiveRecord::Schema.define(version: 20150217123345) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -410,12 +410,12 @@
   end
 
   create_table "users", force: true do |t|
-    t.string   "email",                      default: "",    null: false
-    t.string   "encrypted_password",         default: "",    null: false
+    t.string   "email",                         default: "",    null: false
+    t.string   "encrypted_password",            default: "",    null: false
     t.string   "reset_password_token"
     t.datetime "reset_password_sent_at"
     t.datetime "remember_created_at"
-    t.integer  "sign_in_count",              default: 0
+    t.integer  "sign_in_count",                 default: 0
     t.datetime "current_sign_in_at"
     t.datetime "last_sign_in_at"
     t.string   "current_sign_in_ip"
@@ -423,22 +423,22 @@
     t.datetime "created_at"
     t.datetime "updated_at"
     t.string   "name"
-    t.boolean  "admin",                      default: false, null: false
-    t.integer  "projects_limit",             default: 10
-    t.string   "skype",                      default: "",    null: false
-    t.string   "linkedin",                   default: "",    null: false
-    t.string   "twitter",                    default: "",    null: false
+    t.boolean  "admin",                         default: false, null: false
+    t.integer  "projects_limit",                default: 10
+    t.string   "skype",                         default: "",    null: false
+    t.string   "linkedin",                      default: "",    null: false
+    t.string   "twitter",                       default: "",    null: false
     t.string   "authentication_token"
-    t.integer  "theme_id",                   default: 1,     null: false
+    t.integer  "theme_id",                      default: 1,     null: false
     t.string   "bio"
-    t.integer  "failed_attempts",            default: 0
+    t.integer  "failed_attempts",               default: 0
     t.datetime "locked_at"
     t.string   "username"
-    t.boolean  "can_create_group",           default: true,  null: false
-    t.boolean  "can_create_team",            default: true,  null: false
+    t.boolean  "can_create_group",              default: true,  null: false
+    t.boolean  "can_create_team",               default: true,  null: false
     t.string   "state"
-    t.integer  "color_scheme_id",            default: 1,     null: false
-    t.integer  "notification_level",         default: 1,     null: false
+    t.integer  "color_scheme_id",               default: 1,     null: false
+    t.integer  "notification_level",            default: 1,     null: false
     t.datetime "password_expires_at"
     t.integer  "created_by_id"
     t.datetime "last_credential_check_at"
@@ -447,13 +447,15 @@
     t.datetime "confirmed_at"
     t.datetime "confirmation_sent_at"
     t.string   "unconfirmed_email"
-    t.boolean  "hide_no_ssh_key",            default: false
-    t.string   "website_url",                default: "",    null: false
+    t.boolean  "hide_no_ssh_key",               default: false
+    t.string   "website_url",                   default: "",    null: false
     t.string   "github_access_token"
     t.string   "gitlab_access_token"
     t.string   "notification_email"
-    t.boolean  "hide_no_password",           default: false
-    t.boolean  "password_automatically_set", default: false
+    t.boolean  "hide_no_password",              default: false
+    t.boolean  "password_automatically_set",    default: false
+    t.string   "bitbucket_access_token"
+    t.string   "bitbucket_access_token_secret"
   end
 
   add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
diff --git a/doc/integration/bitbucket.m b/doc/integration/bitbucket.m
new file mode 100644
index 0000000000000..30404ce4c5463
--- /dev/null
+++ b/doc/integration/bitbucket.m
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb
new file mode 100644
index 0000000000000..3d2ef78ee7dac
--- /dev/null
+++ b/lib/gitlab/bitbucket_import/client.rb
@@ -0,0 +1,88 @@
+module Gitlab
+  module BitbucketImport
+    class Client
+      attr_reader :consumer, :api
+
+      def initialize(access_token = nil, access_token_secret = nil)
+        @consumer = ::OAuth::Consumer.new(
+          config.app_id,
+          config.app_secret,
+          bitbucket_options
+        )
+
+        if access_token && access_token_secret
+          @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
+        end
+      end
+
+      def request_token(redirect_uri)
+        request_token = consumer.get_request_token(oauth_callback: redirect_uri)
+
+        {
+          oauth_token:              request_token.token,
+          oauth_token_secret:       request_token.secret,
+          oauth_callback_confirmed: request_token.callback_confirmed?.to_s
+        }
+      end
+
+      def authorize_url(request_token, redirect_uri)
+        request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
+
+        if request_token.callback_confirmed?
+          request_token.authorize_url
+        else
+          request_token.authorize_url(oauth_callback: redirect_uri)
+        end
+      end
+
+      def get_token(request_token, oauth_verifier, redirect_uri)
+        request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
+
+        if request_token.callback_confirmed?
+          request_token.get_access_token(oauth_verifier: oauth_verifier)
+        else
+          request_token.get_access_token(oauth_callback: redirect_uri)
+        end
+      end
+
+      def user
+        JSON.parse(api.get("/api/1.0/user").body)
+      end
+
+      def issues(project_identifier)
+        JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues").body)
+      end
+
+      def issue_comments(project_identifier, issue_id)
+        JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body)
+      end
+
+      def project(project_identifier)
+        JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}").body)
+      end
+
+      def deploy_key(project_identifier)
+        JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find { |key| key["label"] =~ /GitLab/ }
+      end
+
+      def add_deploy_key(project_identifier, key)
+        JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body)
+      end
+
+      def projects
+        JSON.parse(api.get("/api/1.0/user/repositories").body).
+          select { |repo| repo["scm"] == "git" }
+      end
+
+      private
+
+      def config
+        Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket"}
+      end
+
+      def bitbucket_options
+        OmniAuth::Strategies::Bitbucket.default_options[:client_options]
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
new file mode 100644
index 0000000000000..42c93707caa69
--- /dev/null
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -0,0 +1,52 @@
+module Gitlab
+  module BitbucketImport
+    class Importer
+      attr_reader :project, :client
+
+      def initialize(project)
+        @project = project
+        @client = Client.new(project.creator.bitbucket_access_token, project.creator.bitbucket_access_token_secret)
+        @formatter = Gitlab::ImportFormatter.new
+      end
+
+      def execute
+        project_identifier = project.import_source
+
+        return true unless client.project(project_identifier)["has_issues"]
+
+        #Issues && Comments
+        issues = client.issues(project_identifier)
+        
+        issues["issues"].each do |issue|
+          body = @formatter.author_line(issue["reported_by"]["username"], issue["content"])
+          
+          comments = client.issue_comments(project_identifier, issue["local_id"])
+          
+          if comments.any?
+            body += @formatter.comments_header
+          end
+
+          comments.each do |comment|
+            body += @formatter.comment(comment["author_info"]["username"], comment["utc_created_on"], comment["content"])
+          end
+
+          project.issues.create!(
+            description: body, 
+            title: issue["title"],
+            state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened',
+            author_id: gl_user_id(project, issue["reported_by"]["username"])
+          )
+        end
+        
+        true
+      end
+
+      private
+
+      def gl_user_id(project, bitbucket_id)
+        user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
+        (user && user.id) || project.creator_id
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb
new file mode 100644
index 0000000000000..207811237ba50
--- /dev/null
+++ b/lib/gitlab/bitbucket_import/key_adder.rb
@@ -0,0 +1,22 @@
+module Gitlab
+  module BitbucketImport
+    class KeyAdder
+      attr_reader :repo, :current_user, :client
+
+      def initialize(repo, current_user)
+        @repo, @current_user = repo, current_user
+        @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret)
+      end
+
+      def execute
+        project_identifier = "#{repo["owner"]}/#{repo["slug"]}"
+        return true if client.deploy_key(project_identifier)
+
+        # TODO: Point to actual public key.
+        client.add_deploy_key(project_identifier, File.read("/Users/douwemaan/.ssh/id_rsa.pub"))
+
+        true
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb
new file mode 100644
index 0000000000000..db33af2c2dae9
--- /dev/null
+++ b/lib/gitlab/bitbucket_import/project_creator.rb
@@ -0,0 +1,39 @@
+module Gitlab
+  module BitbucketImport
+    class ProjectCreator
+      attr_reader :repo, :namespace, :current_user
+
+      def initialize(repo, namespace, current_user)
+        @repo = repo
+        @namespace = namespace
+        @current_user = current_user
+      end
+
+      def execute
+        @project = Project.new(
+          name: repo["name"],
+          path: repo["slug"],
+          description: repo["description"],
+          namespace: namespace,
+          creator: current_user,
+          visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
+          import_type: "bitbucket",
+          import_source: "#{repo["owner"]}/#{repo["slug"]}",
+          import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git"
+        )
+
+        if @project.save!
+          @project.reload
+
+          if @project.import_failed?
+            @project.import_retry
+          else
+            @project.import_start
+          end
+        end
+
+        @project
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index c9904fe8779a4..676d226bddd7e 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -46,11 +46,7 @@ def config
       end
 
       def github_options
-        {
-          site: 'https://api.github.com',
-          authorize_url: 'https://github.com/login/oauth/authorize',
-          token_url: 'https://github.com/login/oauth/access_token'
-        }
+        OmniAuth::Strategies::GitHub.default_options[:client_options]
       end
     end
   end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index bc2b645b2d91c..23832b3233ce0 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -20,7 +20,7 @@ def execute
               body += @formatter.comments_header
 
               client.issue_comments(project.import_source, issue.number).each do |c|
-                body += @formatter.comment_to_md(c.user.login, c.created_at, c.body)
+                body += @formatter.comment(c.user.login, c.created_at, c.body)
               end
             end
 
diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb
index 2206b68da9920..ecf4ff94e39de 100644
--- a/lib/gitlab/gitlab_import/client.rb
+++ b/lib/gitlab/gitlab_import/client.rb
@@ -9,7 +9,7 @@ def initialize(access_token)
         @client = ::OAuth2::Client.new(
           config.app_id,
           config.app_secret,
-          github_options
+          gitlab_options
         )
 
         if access_token
@@ -70,12 +70,8 @@ def config
         Gitlab.config.omniauth.providers.find{|provider| provider.name == "gitlab"}
       end
 
-      def github_options
-        {
-          site: 'https://gitlab.com/',
-          authorize_url: 'oauth/authorize',
-          token_url: 'oauth/token'
-        }
+      def gitlab_options
+        OmniAuth::Strategies::GitLab.default_options[:client_options]
       end
     end
   end
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index 5f9b14399a450..c5304a0699b9e 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -25,7 +25,7 @@ def execute
           end
 
           comments.each do |comment|
-            body += @formatter.comment_to_md(comment["author"]["name"], comment["created_at"], comment["body"])
+            body += @formatter.comment(comment["author"]["name"], comment["created_at"], comment["body"])
           end
 
           project.issues.create!(
diff --git a/lib/gitlab/import_formatter.rb b/lib/gitlab/import_formatter.rb
index ebb4b87f7e33a..72e041a90b1d5 100644
--- a/lib/gitlab/import_formatter.rb
+++ b/lib/gitlab/import_formatter.rb
@@ -1,6 +1,6 @@
 module Gitlab
   class ImportFormatter
-    def comment_to_md(author, date, body)
+    def comment(author, date, body)
       "\n\n*By #{author} on #{date}*\n\n#{body}"
     end
 
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
new file mode 100644
index 0000000000000..84e37ae560764
--- /dev/null
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Import::BitbucketController do
+  let(:user) { create(:user, bitbucket_access_token: 'asd123', bitbucket_access_token_secret: "sekret") }
+
+  before do
+    sign_in(user)
+  end
+
+  describe "GET callback" do
+    before do
+      session[:oauth_request_token] = {}
+    end
+    
+    it "updates access token" do
+      token = "asdasd12345"
+      secret = "sekrettt"
+      access_token = double(token: token, secret: secret)
+      Gitlab::BitbucketImport::Client.any_instance.stub(:get_token).and_return(access_token)
+      Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket")
+
+      get :callback
+
+      expect(user.reload.bitbucket_access_token).to eq(token)
+      expect(user.reload.bitbucket_access_token_secret).to eq(secret)
+      expect(controller).to redirect_to(status_import_bitbucket_url)
+    end
+  end
+
+  describe "GET status" do
+    before do
+      @repo = OpenStruct.new(slug: 'vim', owner: 'asd')
+    end
+
+    it "assigns variables" do
+      @project = create(:project, import_type: 'bitbucket', creator_id: user.id)
+      controller.stub_chain(:client, :projects).and_return([@repo])
+
+      get :status
+
+      expect(assigns(:already_added_projects)).to eq([@project])
+      expect(assigns(:repos)).to eq([@repo])
+    end
+
+    it "does not show already added project" do
+      @project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
+      controller.stub_chain(:client, :projects).and_return([@repo])
+
+      get :status
+
+      expect(assigns(:already_added_projects)).to eq([@project])
+      expect(assigns(:repos)).to eq([])
+    end
+  end
+
+  describe "POST create" do
+    before do
+      @repo = {
+        slug: 'vim',
+        owner: "john"
+      }.with_indifferent_access
+    end
+
+    it "takes already existing namespace" do
+      namespace = create(:namespace, name: "john", owner: user)
+      expect(Gitlab::BitbucketImport::KeyAdder).
+        to receive(:new).with(@repo, user).
+        and_return(double(execute: true))
+      expect(Gitlab::BitbucketImport::ProjectCreator).
+        to receive(:new).with(@repo, namespace, user).
+        and_return(double(execute: true))
+      controller.stub_chain(:client, :project).and_return(@repo)
+
+      post :create, format: :js
+    end
+  end
+end
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
new file mode 100644
index 0000000000000..f5523105848ce
--- /dev/null
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::BitbucketImport::ProjectCreator do
+  let(:user) { create(:user, bitbucket_access_token: "asdffg", bitbucket_access_token_secret: "sekret") }
+  let(:repo) { {
+    name: 'Vim',
+    slug: 'vim',
+    is_private: true,
+    owner: "asd"}.with_indifferent_access
+  }
+  let(:namespace){ create(:namespace) }
+
+  it 'creates project' do
+    allow_any_instance_of(Project).to receive(:add_import_job)
+    
+    project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user)
+    project = project_creator.execute
+    
+    expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git")
+    expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+  end
+end
diff --git a/spec/lib/gitlab/github/project_creator.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
similarity index 77%
rename from spec/lib/gitlab/github/project_creator.rb
rename to spec/lib/gitlab/github_import/project_creator_spec.rb
index 3686ddbf17015..8d594a112d464 100644
--- a/spec/lib/gitlab/github/project_creator.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe Gitlab::Github::ProjectCreator do
+describe Gitlab::GithubImport::ProjectCreator do
   let(:user) { create(:user, github_access_token: "asdffg") }
   let(:repo) { OpenStruct.new(
     login: 'vim',
@@ -15,9 +15,8 @@
   it 'creates project' do
     allow_any_instance_of(Project).to receive(:add_import_job)
     
-    project_creator = Gitlab::Github::ProjectCreator.new(repo, namespace, user)
-    project_creator.execute
-    project = Project.last
+    project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user)
+    project = project_creator.execute
     
     expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git")
     expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
diff --git a/spec/lib/gitlab/gitlab_import/project_creator.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
similarity index 91%
rename from spec/lib/gitlab/gitlab_import/project_creator.rb
rename to spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index e5d917830b045..4c0d64ed13899 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -2,7 +2,7 @@
 
 describe Gitlab::GitlabImport::ProjectCreator do
   let(:user) { create(:user, gitlab_access_token: "asdffg") }
-  let(:repo) {{
+  let(:repo) { {
     name: 'vim',
     path: 'vim',
     visibility_level: Gitlab::VisibilityLevel::PRIVATE,
@@ -16,8 +16,7 @@
     allow_any_instance_of(Project).to receive(:add_import_job)
     
     project_creator = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, user)
-    project_creator.execute
-    project = Project.last
+    project = project_creator.execute
     
     expect(project.import_url).to eq("https://oauth2:asdffg@gitlab.com/asd/vim.git")
     expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
-- 
GitLab