summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStan Hu <stanhu@gmail.com>2018-06-25 13:06:10 -0700
committerStan Hu <stanhu@gmail.com>2018-06-25 13:06:10 -0700
commitebd8e4333a263138abf2113dd315a97352851cbe (patch)
tree4e4975eab778d490c20c9252785761a1d78fae26
parent2bac2918b2d6f12d94f739f4b6865b9e9221c642 (diff)
downloadgitlab-ce-ebd8e4333a263138abf2113dd315a97352851cbe.tar.gz
WIP: Add support for Bitbucket Server imports
-rw-r--r--app/controllers/application_controller.rb6
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb92
-rw-r--r--app/views/projects/_import_project_pane.html.haml7
-rw-r--r--config/routes/import.rb7
-rw-r--r--lib/bitbucket_server/client.rb59
-rw-r--r--lib/bitbucket_server/connection.rb35
-rw-r--r--lib/bitbucket_server/page.rb34
-rw-r--r--lib/bitbucket_server/paginator.rb36
-rw-r--r--lib/bitbucket_server/representation/base.rb15
-rw-r--r--lib/bitbucket_server/representation/comment.rb27
-rw-r--r--lib/bitbucket_server/representation/issue.rb53
-rw-r--r--lib/bitbucket_server/representation/pull_request.rb65
-rw-r--r--lib/bitbucket_server/representation/pull_request_comment.rb39
-rw-r--r--lib/bitbucket_server/representation/repo.rb71
-rw-r--r--lib/bitbucket_server/representation/user.rb9
-rw-r--r--lib/gitlab/import_sources.rb3
16 files changed, 555 insertions, 3 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 21cc6dfdd16..f864fbcaa43 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, prepend: true
helper_method :can?
- helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
+ helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?, :bitbucket_server_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -307,6 +307,10 @@ class ApplicationController < ActionController::Base
!Gitlab::CurrentSettings.import_sources.empty?
end
+ def bitbucket_server_import_enabled?
+ Gitlab::CurrentSettings.import_sources.include?('bitbucket_server')
+ end
+
def github_import_enabled?
Gitlab::CurrentSettings.import_sources.include?('github')
end
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
new file mode 100644
index 00000000000..3662708c890
--- /dev/null
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -0,0 +1,92 @@
+class Import::BitbucketServerController < Import::BaseController
+ before_action :verify_bitbucket_server_import_enabled
+ before_action :bitbucket_auth, except: [:new, :configure]
+
+ def new
+ end
+
+ def create
+ bitbucket_client = BitbucketServer::Client.new(credentials)
+
+ repo_id = params[:repo_id].to_s
+ # XXX must be a better way
+ project_slug, repo_slug = repo_id.split("___")
+ repo = bitbucket_client.repo(project_slug, repo_slug)
+ project_name = params[:new_name].presence || repo.name
+
+ repo_owner = repo.owner
+ repo_owner = current_user.username if repo_owner == bitbucket_client.user.username
+ namespace_path = params[:new_namespace].presence || repo_owner
+ target_namespace = find_or_create_namespace(namespace_path, current_user)
+
+ if current_user.can?(:create_projects, target_namespace)
+ project = Gitlab::BitbucketImport::ProjectCreator.new(repo, project_name, target_namespace, current_user, credentials).execute
+
+ if project.persisted?
+ render json: ProjectSerializer.new.represent(project)
+ else
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
+ end
+ else
+ render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
+ end
+ end
+
+ def configure
+ session[personal_access_token_key] = params[:personal_access_token]
+ session[bitbucket_server_username_key] = params[:bitbucket_username]
+ session[bitbucket_server_url_key] = params[:bitbucket_server_url]
+
+ redirect_to status_import_bitbucket_server_path
+ end
+
+ def status
+ bitbucket_client = BitbucketServer::Client.new(credentials)
+ repos = bitbucket_client.repos
+
+ @repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
+
+ @already_added_projects = find_already_added_projects('bitbucket_server')
+ already_added_projects_names = @already_added_projects.pluck(:import_source)
+
+ @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
+ end
+
+ def jobs
+ render json: find_jobs('bitbucket_server')
+ end
+
+ private
+
+ def bitbucket_auth
+ unless session[bitbucket_server_url_key].present? &&
+ session[bitbucket_server_username_key].present? &&
+ session[personal_access_token_key].present?
+ redirect_to new_import_bitbucket_server_path
+ end
+ end
+
+ def verify_bitbucket_server_import_enabled
+ render_404 unless bitbucket_server_import_enabled?
+ end
+
+ def bitbucket_server_url_key
+ :bitbucket_server_url
+ end
+
+ def bitbucket_server_username_key
+ :bitbucket_server_username
+ end
+
+ def personal_access_token_key
+ :bitbucket_server_personal_access_token
+ end
+
+ def credentials
+ {
+ base_uri: session[bitbucket_server_url_key],
+ username: session[bitbucket_server_username_key],
+ personal_access_token: session[personal_access_token_key]
+ }
+ end
+end
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 8f535b9d789..ccb724a3b0f 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -17,10 +17,15 @@
%div
- if bitbucket_import_enabled?
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
- = icon('bitbucket', text: 'Bitbucket')
+ = icon('bitbucket', text: 'Bitbucket Cloud')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
%div
+ - if bitbucket_server_import_enabled?
+ = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket" do
+ = icon('bitbucket', text: 'Bitbucket Server')
+ = render 'bitbucket_import_modal'
+ %div
- if gitlab_import_enabled?
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= icon('gitlab', text: 'GitLab.com')
diff --git a/config/routes/import.rb b/config/routes/import.rb
index c378253bf15..be3058193d5 100644
--- a/config/routes/import.rb
+++ b/config/routes/import.rb
@@ -24,6 +24,13 @@ namespace :import do
get :jobs
end
+ resource :bitbucket_server, only: [:create, :new], controller: :bitbucket_server do
+ post :configure
+ get :status
+ get :callback
+ get :jobs
+ end
+
resource :google_code, only: [:create, :new], controller: :google_code do
get :status
post :callback
diff --git a/lib/bitbucket_server/client.rb b/lib/bitbucket_server/client.rb
new file mode 100644
index 00000000000..1f2f03790dd
--- /dev/null
+++ b/lib/bitbucket_server/client.rb
@@ -0,0 +1,59 @@
+module BitbucketServer
+ class Client
+ attr_reader :connection
+
+ def initialize(options = {})
+ @connection = Connection.new(options)
+ end
+
+ def issues(repo)
+ path = "/repositories/#{repo}/issues"
+ get_collection(path, :issue)
+ end
+
+ def issue_comments(repo, issue_id)
+ path = "/repositories/#{repo}/issues/#{issue_id}/comments"
+ get_collection(path, :comment)
+ end
+
+ def pull_requests(repo)
+ path = "/repositories/#{repo}/pullrequests?state=ALL"
+ get_collection(path, :pull_request)
+ end
+
+ def pull_request_comments(repo, pull_request)
+ path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments"
+ get_collection(path, :pull_request_comment)
+ end
+
+ def pull_request_diff(repo, pull_request)
+ path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff"
+ connection.get(path)
+ end
+
+ def repo(project, repo_name)
+ parsed_response = connection.get("/projects/#{project}/repos/#{repo_name}")
+ # XXXX TODO Handle failure
+ BitbucketServer::Representation::Repo.new(parsed_response)
+ end
+
+ def repos
+ path = "/repos"
+ get_collection(path, :repo)
+ end
+
+ def user
+ @user ||= begin
+ parsed_response = connection.get('/user')
+ BitbucketServer::Representation::User.new(parsed_response)
+ end
+ end
+
+ private
+
+ def get_collection(path, type)
+ paginator = BitbucketServer::Paginator.new(connection, path, type)
+ BitbucketServer::Collection.new(paginator)
+ end
+ end
+end
diff --git a/lib/bitbucket_server/connection.rb b/lib/bitbucket_server/connection.rb
new file mode 100644
index 00000000000..64f12527a8d
--- /dev/null
+++ b/lib/bitbucket_server/connection.rb
@@ -0,0 +1,35 @@
+module BitbucketServer
+ class Connection
+ DEFAULT_API_VERSION = '1.0'.freeze
+
+ attr_reader :api_version, :base_uri, :username, :token
+
+ def initialize(options = {})
+ @api_version = options.fetch(:api_version, DEFAULT_API_VERSION)
+ @base_uri = options[:base_uri]
+ @username = options[:username]
+ @token = options[:personal_access_token]
+ end
+
+ def get(path, extra_query = {})
+ auth = { username: username, password: token }
+ response = Gitlab::HTTP.get(build_url(path),
+ basic_auth: auth,
+ params: extra_query)
+ ## Handle failure
+ response.parsed_response
+ end
+
+ private
+
+ def build_url(path)
+ return path if path.starts_with?(root_url)
+
+ "#{root_url}#{path}"
+ end
+
+ def root_url
+ "#{base_uri}/rest/api/#{api_version}"
+ end
+ end
+end
diff --git a/lib/bitbucket_server/page.rb b/lib/bitbucket_server/page.rb
new file mode 100644
index 00000000000..17be8cbb860
--- /dev/null
+++ b/lib/bitbucket_server/page.rb
@@ -0,0 +1,34 @@
+module BitbucketServer
+ class Page
+ attr_reader :attrs, :items
+
+ def initialize(raw, type)
+ @attrs = parse_attrs(raw)
+ @items = parse_values(raw, representation_class(type))
+ end
+
+ def next?
+ !attrs.fetch(:isLastPage, true)
+ end
+
+ def next
+ attrs.fetch(:nextPageStart)
+ end
+
+ private
+
+ def parse_attrs(raw)
+ raw.slice(*%w(size nextPageStart isLastPage)).symbolize_keys
+ end
+
+ def parse_values(raw, bitbucket_rep_class)
+ return [] unless raw['values'] && raw['values'].is_a?(Array)
+
+ bitbucket_rep_class.decorate(raw['values'])
+ end
+
+ def representation_class(type)
+ BitbucketServer::Representation.const_get(type.to_s.camelize)
+ end
+ end
+end
diff --git a/lib/bitbucket_server/paginator.rb b/lib/bitbucket_server/paginator.rb
new file mode 100644
index 00000000000..c995cf4c3bd
--- /dev/null
+++ b/lib/bitbucket_server/paginator.rb
@@ -0,0 +1,36 @@
+module BitbucketServer
+ class Paginator
+ PAGE_LENGTH = 25 # The minimum length is 10 and the maximum is 100.
+
+ def initialize(connection, url, type)
+ @connection = connection
+ @type = type
+ @url = url
+ @page = nil
+ end
+
+ def items
+ raise StopIteration unless has_next_page?
+
+ @page = fetch_next_page
+ @page.items
+ end
+
+ private
+
+ attr_reader :connection, :page, :url, :type
+
+ def has_next_page?
+ page.nil? || page.next?
+ end
+
+ def next_url
+ page.nil? ? url : page.next
+ end
+
+ def fetch_next_page
+ parsed_response = connection.get(next_url, pagelen: PAGE_LENGTH, sort: :created_on)
+ Page.new(parsed_response, type)
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/base.rb b/lib/bitbucket_server/representation/base.rb
new file mode 100644
index 00000000000..11b32b70c4c
--- /dev/null
+++ b/lib/bitbucket_server/representation/base.rb
@@ -0,0 +1,15 @@
+module BitbucketServer
+ module Representation
+ class Base
+ attr_reader :raw
+
+ def initialize(raw)
+ @raw = raw
+ end
+
+ def self.decorate(entries)
+ entries.map { |entry| new(entry)}
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/comment.rb b/lib/bitbucket_server/representation/comment.rb
new file mode 100644
index 00000000000..4937aa9728f
--- /dev/null
+++ b/lib/bitbucket_server/representation/comment.rb
@@ -0,0 +1,27 @@
+module Bitbucket
+ module Representation
+ class Comment < Representation::Base
+ def author
+ user['username']
+ end
+
+ def note
+ raw.fetch('content', {}).fetch('raw', nil)
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['updated_on'] || raw['created_on']
+ end
+
+ private
+
+ def user
+ raw.fetch('user', {})
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/issue.rb b/lib/bitbucket_server/representation/issue.rb
new file mode 100644
index 00000000000..44bcbc250b3
--- /dev/null
+++ b/lib/bitbucket_server/representation/issue.rb
@@ -0,0 +1,53 @@
+module Bitbucket
+ module Representation
+ class Issue < Representation::Base
+ CLOSED_STATUS = %w(resolved invalid duplicate wontfix closed).freeze
+
+ def iid
+ raw['id']
+ end
+
+ def kind
+ raw['kind']
+ end
+
+ def author
+ raw.dig('reporter', 'username')
+ end
+
+ def description
+ raw.fetch('content', {}).fetch('raw', nil)
+ end
+
+ def state
+ closed? ? 'closed' : 'opened'
+ end
+
+ def title
+ raw['title']
+ end
+
+ def milestone
+ raw['milestone']['name'] if raw['milestone'].present?
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['edited_on']
+ end
+
+ def to_s
+ iid
+ end
+
+ private
+
+ def closed?
+ CLOSED_STATUS.include?(raw['state'])
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/pull_request.rb b/lib/bitbucket_server/representation/pull_request.rb
new file mode 100644
index 00000000000..3553f3adbc7
--- /dev/null
+++ b/lib/bitbucket_server/representation/pull_request.rb
@@ -0,0 +1,65 @@
+module BitbucketServer
+ module Representation
+ class PullRequest < Representation::Base
+ def author
+ raw.fetch('author', {}).fetch('username', nil)
+ end
+
+ def description
+ raw['description']
+ end
+
+ def iid
+ raw['id']
+ end
+
+ def state
+ if raw['state'] == 'MERGED'
+ 'merged'
+ elsif raw['state'] == 'DECLINED'
+ 'closed'
+ else
+ 'opened'
+ end
+ end
+
+ def created_at
+ raw['created_on']
+ end
+
+ def updated_at
+ raw['updated_on']
+ end
+
+ def title
+ raw['title']
+ end
+
+ def source_branch_name
+ source_branch.fetch('branch', {}).fetch('name', nil)
+ end
+
+ def source_branch_sha
+ source_branch.fetch('commit', {}).fetch('hash', nil)
+ end
+
+ def target_branch_name
+ target_branch.fetch('branch', {}).fetch('name', nil)
+ end
+
+ def target_branch_sha
+ target_branch.fetch('commit', {}).fetch('hash', nil)
+ end
+
+ private
+
+ def source_branch
+ raw['source']
+ end
+
+ def target_branch
+ raw['destination']
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/pull_request_comment.rb b/lib/bitbucket_server/representation/pull_request_comment.rb
new file mode 100644
index 00000000000..c52acbc3ddc
--- /dev/null
+++ b/lib/bitbucket_server/representation/pull_request_comment.rb
@@ -0,0 +1,39 @@
+module Bitbucket
+ module Representation
+ class PullRequestComment < Comment
+ def iid
+ raw['id']
+ end
+
+ def file_path
+ inline.fetch('path')
+ end
+
+ def old_pos
+ inline.fetch('from')
+ end
+
+ def new_pos
+ inline.fetch('to')
+ end
+
+ def parent_id
+ raw.fetch('parent', {}).fetch('id', nil)
+ end
+
+ def inline?
+ raw.key?('inline')
+ end
+
+ def has_parent?
+ raw.key?('parent')
+ end
+
+ private
+
+ def inline
+ raw.fetch('inline', {})
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/repo.rb b/lib/bitbucket_server/representation/repo.rb
new file mode 100644
index 00000000000..f4bdb277d28
--- /dev/null
+++ b/lib/bitbucket_server/representation/repo.rb
@@ -0,0 +1,71 @@
+module BitbucketServer
+ module Representation
+ class Repo < Representation::Base
+ attr_reader :owner, :slug
+
+ def initialize(raw)
+ super(raw)
+ end
+
+ def owner
+ project['name']
+ end
+
+ def slug
+ raw['slug']
+ end
+
+ def clone_url(token = nil)
+ url = raw['links']['clone'].find { |link| link['name'].starts_with?('http') }.fetch('href')
+
+ if token.present?
+ clone_url = URI.parse(url)
+ clone_url.user = "x-token-auth:#{token}"
+ clone_url.to_s
+ else
+ url
+ end
+ end
+
+ def description
+ project['description']
+ end
+
+ def full_name
+ "#{owner}/#{name}"
+ end
+
+ def issues_enabled?
+ true
+ end
+
+ def name
+ raw['name']
+ end
+
+ def valid?
+ raw['scmId'] == 'git'
+ end
+
+ def has_wiki?
+ false
+ end
+
+ def visibility_level
+ if project['public']
+ Gitlab::VisibilityLevel::PUBLIC
+ else
+ Gitlab::VisibilityLevel::PRIVATE
+ end
+ end
+
+ def project
+ raw['project']
+ end
+
+ def to_s
+ full_name
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/user.rb b/lib/bitbucket_server/representation/user.rb
new file mode 100644
index 00000000000..174f3a55f2c
--- /dev/null
+++ b/lib/bitbucket_server/representation/user.rb
@@ -0,0 +1,9 @@
+module BitbucketServer
+ module Representation
+ class User < Representation::Base
+ def username
+ raw['username']
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 60d5fa4d29a..10289af6b25 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -10,7 +10,8 @@ module Gitlab
# We exclude `bare_repository` here as it has no import class associated
ImportTable = [
ImportSource.new('github', 'GitHub', Gitlab::GithubImport::ParallelImporter),
- ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer),
+ ImportSource.new('bitbucket', 'Bitbucket Cloud', Gitlab::BitbucketImport::Importer),
+ ImportSource.new('bitbucket_server', 'Bitbucket Server', Gitlab::BitbucketServerImport::Importer),
ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer),
ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),