summaryrefslogtreecommitdiff
path: root/lib/bitbucket
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bitbucket')
-rw-r--r--lib/bitbucket/client.rb58
-rw-r--r--lib/bitbucket/collection.rb21
-rw-r--r--lib/bitbucket/connection.rb69
-rw-r--r--lib/bitbucket/error/unauthorized.rb6
-rw-r--r--lib/bitbucket/page.rb34
-rw-r--r--lib/bitbucket/paginator.rb36
-rw-r--r--lib/bitbucket/representation/base.rb17
-rw-r--r--lib/bitbucket/representation/comment.rb27
-rw-r--r--lib/bitbucket/representation/issue.rb53
-rw-r--r--lib/bitbucket/representation/pull_request.rb65
-rw-r--r--lib/bitbucket/representation/pull_request_comment.rb39
-rw-r--r--lib/bitbucket/representation/repo.rb67
-rw-r--r--lib/bitbucket/representation/user.rb9
13 files changed, 501 insertions, 0 deletions
diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb
new file mode 100644
index 00000000000..f8ee7e0f9ae
--- /dev/null
+++ b/lib/bitbucket/client.rb
@@ -0,0 +1,58 @@
+module Bitbucket
+ 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(name)
+ parsed_response = connection.get("/repositories/#{name}")
+ Representation::Repo.new(parsed_response)
+ end
+
+ def repos
+ path = "/repositories?role=member"
+ get_collection(path, :repo)
+ end
+
+ def user
+ @user ||= begin
+ parsed_response = connection.get('/user')
+ Representation::User.new(parsed_response)
+ end
+ end
+
+ private
+
+ def get_collection(path, type)
+ paginator = Paginator.new(connection, path, type)
+ Collection.new(paginator)
+ end
+ end
+end
diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb
new file mode 100644
index 00000000000..3a9379ff680
--- /dev/null
+++ b/lib/bitbucket/collection.rb
@@ -0,0 +1,21 @@
+module Bitbucket
+ class Collection < Enumerator
+ def initialize(paginator)
+ super() do |yielder|
+ loop do
+ paginator.items.each { |item| yielder << item }
+ end
+ end
+
+ lazy
+ end
+
+ def method_missing(method, *args)
+ return super unless self.respond_to?(method)
+
+ self.send(method, *args) do |item|
+ block_given? ? yield(item) : item
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
new file mode 100644
index 00000000000..7e55cf4deab
--- /dev/null
+++ b/lib/bitbucket/connection.rb
@@ -0,0 +1,69 @@
+module Bitbucket
+ class Connection
+ DEFAULT_API_VERSION = '2.0'
+ DEFAULT_BASE_URI = 'https://api.bitbucket.org/'
+ DEFAULT_QUERY = {}
+
+ attr_reader :expires_at, :expires_in, :refresh_token, :token
+
+ def initialize(options = {})
+ @api_version = options.fetch(:api_version, DEFAULT_API_VERSION)
+ @base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI)
+ @default_query = options.fetch(:query, DEFAULT_QUERY)
+
+ @token = options[:token]
+ @expires_at = options[:expires_at]
+ @expires_in = options[:expires_in]
+ @refresh_token = options[:refresh_token]
+ end
+
+ def get(path, extra_query = {})
+ refresh! if expired?
+
+ response = connection.get(build_url(path), params: @default_query.merge(extra_query))
+ response.parsed
+ end
+
+ def expired?
+ connection.expired?
+ end
+
+ def refresh!
+ response = connection.refresh!
+
+ @token = response.token
+ @expires_at = response.expires_at
+ @expires_in = response.expires_in
+ @refresh_token = response.refresh_token
+ @connection = nil
+ end
+
+ private
+
+ def client
+ @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
+ end
+
+ def connection
+ @connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in)
+ end
+
+ def build_url(path)
+ return path if path.starts_with?(root_url)
+
+ "#{root_url}#{path}"
+ end
+
+ def root_url
+ @root_url ||= "#{@base_uri}#{@api_version}"
+ end
+
+ def provider
+ Gitlab::OAuth::Provider.config_for('bitbucket')
+ end
+
+ def options
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
+ end
+ end
+end
diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb
new file mode 100644
index 00000000000..5e2eb57bb0e
--- /dev/null
+++ b/lib/bitbucket/error/unauthorized.rb
@@ -0,0 +1,6 @@
+module Bitbucket
+ module Error
+ class Unauthorized < StandardError
+ end
+ end
+end
diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb
new file mode 100644
index 00000000000..2b0a3fe7b1a
--- /dev/null
+++ b/lib/bitbucket/page.rb
@@ -0,0 +1,34 @@
+module Bitbucket
+ 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(:next, false)
+ end
+
+ def next
+ attrs.fetch(:next)
+ end
+
+ private
+
+ def parse_attrs(raw)
+ raw.slice(*%w(size page pagelen next previous)).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)
+ Bitbucket::Representation.const_get(type.to_s.camelize)
+ end
+ end
+end
diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb
new file mode 100644
index 00000000000..135d0d55674
--- /dev/null
+++ b/lib/bitbucket/paginator.rb
@@ -0,0 +1,36 @@
+module Bitbucket
+ class Paginator
+ PAGE_LENGTH = 50 # 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/representation/base.rb b/lib/bitbucket/representation/base.rb
new file mode 100644
index 00000000000..94adaacc9b5
--- /dev/null
+++ b/lib/bitbucket/representation/base.rb
@@ -0,0 +1,17 @@
+module Bitbucket
+ module Representation
+ class Base
+ def initialize(raw)
+ @raw = raw
+ end
+
+ def self.decorate(entries)
+ entries.map { |entry| new(entry)}
+ end
+
+ private
+
+ attr_reader :raw
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb
new file mode 100644
index 00000000000..4937aa9728f
--- /dev/null
+++ b/lib/bitbucket/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/representation/issue.rb b/lib/bitbucket/representation/issue.rb
new file mode 100644
index 00000000000..054064395c3
--- /dev/null
+++ b/lib/bitbucket/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.fetch('reporter', {}).fetch('username', nil)
+ 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/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb
new file mode 100644
index 00000000000..eebf8093380
--- /dev/null
+++ b/lib/bitbucket/representation/pull_request.rb
@@ -0,0 +1,65 @@
+module Bitbucket
+ 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/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb
new file mode 100644
index 00000000000..4f8efe03bae
--- /dev/null
+++ b/lib/bitbucket/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.has_key?('inline')
+ end
+
+ def has_parent?
+ raw.has_key?('parent')
+ end
+
+ private
+
+ def inline
+ raw.fetch('inline', {})
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
new file mode 100644
index 00000000000..8969ecd1c19
--- /dev/null
+++ b/lib/bitbucket/representation/repo.rb
@@ -0,0 +1,67 @@
+module Bitbucket
+ module Representation
+ class Repo < Representation::Base
+ attr_reader :owner, :slug
+
+ def initialize(raw)
+ super(raw)
+ end
+
+ def owner_and_slug
+ @owner_and_slug ||= full_name.split('/', 2)
+ end
+
+ def owner
+ owner_and_slug.first
+ end
+
+ def slug
+ owner_and_slug.last
+ end
+
+ def clone_url(token = nil)
+ url = raw['links']['clone'].find { |link| link['name'] == 'https' }.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
+ raw['description']
+ end
+
+ def full_name
+ raw['full_name']
+ end
+
+ def issues_enabled?
+ raw['has_issues']
+ end
+
+ def name
+ raw['name']
+ end
+
+ def valid?
+ raw['scm'] == 'git'
+ end
+
+ def visibility_level
+ if raw['is_private']
+ Gitlab::VisibilityLevel::PRIVATE
+ else
+ Gitlab::VisibilityLevel::PUBLIC
+ end
+ end
+
+ def to_s
+ full_name
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket/representation/user.rb b/lib/bitbucket/representation/user.rb
new file mode 100644
index 00000000000..ba6b7667b49
--- /dev/null
+++ b/lib/bitbucket/representation/user.rb
@@ -0,0 +1,9 @@
+module Bitbucket
+ module Representation
+ class User < Representation::Base
+ def username
+ raw['username']
+ end
+ end
+ end
+end