Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions git-review.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'hashie'
s.add_runtime_dependency 'gli', '~> 2.8.0'
s.add_runtime_dependency 'octokit', '~> 2.7.2'
s.add_runtime_dependency 'gitlab', '~> 3.0.0'
end
2 changes: 2 additions & 0 deletions lib/git-review.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
# Add some POROs to get some structure into the entities git-review deals with.
require_relative 'models/base'
require_relative 'models/repository'
require_relative 'models/forked_repository'
require_relative 'models/user'
require_relative 'models/commit'
require_relative 'models/request'
Expand All @@ -41,3 +42,4 @@
require_relative 'git-review/provider/github/request'
require_relative 'git-review/provider/github/commit'
require_relative 'git-review/provider/bitbucket/bitbucket'
require_relative 'git-review/provider/gitlab'
360 changes: 360 additions & 0 deletions lib/git-review/provider/gitlab.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
require 'gitlab'
require 'uri'

module GitReview

module Provider

class Gitlab < Base
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid with the latest code in the development branch a lot of this won't work anymore. I'm trying to pull as much functionality out of the provider specific part and move it into the generic base.rb. I'm aiming at refactoring these provider classes into a set of lean methods that basically just create instances of git-review's models (e.g. Request, Commit, ...). If you take a look at the current implementation in github.rb, you might be able to see where I'm going with this. In the current state it's far from finished, though.

I'm still willing to pull your changes in. However, this will probably result in breaking your code (and thus Gitlab support). A couple of examples:

  • the request method doesn't return an instance of Request.
  • commit_discussion will no longer be reachable, as it is implemented in base.rb now.
  • there is no method commits, which is supposed to be querying the providers API and create a collection of Commit instances.

There are quite a lot of changes in the current development branch. In the long run this should make implementing new providers a lot simpler. For the moment however, I think it'd break your code. Which means to actually be able to use git-review with Gitlab, you'd probably have to stick to using the code on your fork until this huge refactoring is done.

Please take a look at my recent work and tell me what you think. Would you rather have me pull in the code (it doesn't break git-review's existing functionality, so I wouldn't mind) or should I wait until I've done my changes to the provider class?


include ::GitReview::Helpers

# @return [String] Authenticated username
def configure_access
if settings[token_key]
@client = ::Gitlab.client(
:endpoint => "https://#{gitlab_host}/api/v3",
:private_token => settings[token_key]
)
else
configure_token
configure_access
end
end

# Find a request by a specified number and return it (or nil otherwise).
def request(number, repo=source_repo)
raise ::GitReview::InvalidRequestIDError unless number
number = number.to_i
attributes = ClientItems.new(client, :merge_requests, project_id(repo)).find do |request|
request.iid == number
end
build_request(attributes, repo)
rescue ::Gitlab::Error::NotFound
raise ::GitReview::InvalidRequestIDError
end

def pull_request(repo, number)
request(number, repo)
end

# @return [Boolean, Hash] the specified request if exists, otherwise false.
# Instead of true, the request itself is returned, so another round-trip
# of merge_request can be avoided.
def request_exists?(number, state='open')
request = request(number)
request && request.state == state
end

def request_exists_for_branch?(upstream = false, branch = local.source_branch)
target_repo = local.target_repo(upstream)
ClientItems.new(client, :merge_requests, project_id(target_repo)).any? { |r| r.source_branch == branch }
end

def requests(repo=source_repo)
ClientItems.new(
client,
:merge_requests,
project_id(repo),
state: 'opened'
).map do |request|
build_request(request, repo)
end.reject do |request|
# Remove invalid and closed/merged merge requests
request.head.sha.nil? || request.state != 'open'
end
end

# a more detailed collection of requests
def current_requests_full(repo=source_repo)
# TODO get comments
requests(repo)
end

def send_pull_request(to_upstream = false)
target_repo = local.target_repo(to_upstream)
base = local.target_branch
title, body = local.create_title_and_body(base)

# gather information before creating pull request
latest_number = latest_request_number(target_repo)

# create the actual pull request
raw_request = client.create_merge_request(
project_id(source_repo),
title,
:description => body,
:target_project_id => project_id(target_repo),
:source_branch => local.source_branch,
:target_branch => base
)
request = build_request(raw_request, target_repo)
# switch back to target_branch and check for success
git_call "checkout #{base}"

# make sure the new pull request is indeed created
new_number = request.number
if new_number && new_number > latest_number
puts "Successfully created new request ##{new_number}"
puts request_url_for target_repo, new_number
else
puts "Pull request was not created for #{target_repo}."
end
end

def close_issue(repo, request_number)
client.update_merge_request(
project_id(repo),
request_number,
:state_event => 'merge'
)
end

def add_comment(repo, request_number, comment)
#TODO
puts 'TODO: Can\'t post comments yet with API'
{:body => comment}
end

def repository(repo)
build_repository(client.project(project_id(repo)))
end

def commit_discussion(number)
pull_commits = client.pull_commits(source_repo, number)
repo = client.pull_request(source_repo, number).head.repo.full_name
discussion = ["Commits on pull request:\n\n"]
discussion += pull_commits.collect { |commit|
# commit message
name = commit.committer.login
output = "\e[35m#{name}\e[m "
output << "committed \e[36m#{commit.sha[0..6]}\e[m "
output << "on #{commit.commit.committer.date.review_time}"
output << ":\n#{''.rjust(output.length + 1, "-")}\n"
output << "#{commit.commit.message}"
output << "\n\n"
result = [output]

# comments on commit
comments = client.commit_comments(repo, commit.sha)
result + comments.collect { |comment|
name = comment.user.login
output = "\e[35m#{name}\e[m "
output << "added a comment to \e[36m#{commit.sha[0..6]}\e[m"
output << " on #{comment.created_at.review_time}"
unless comment.created_at == comment.updated_at
output << " (updated on #{comment.updated_at.review_time})"
end
output << ":\n#{''.rjust(output.length + 1, "-")}\n"
output << comment.body
output << "\n\n"
}
}
discussion.compact.flatten unless discussion.empty?
end

def issue_discussion(number)
comments = client.issue_comments(source_repo, number) +
client.review_comments(source_repo, number)
discussion = ["\nComments on pull request:\n\n"]
discussion += comments.collect { |comment|
name = comment.user.login
output = "\e[35m#{name}\e[m "
output << "added a comment to \e[36m#{comment.id}\e[m"
output << " on #{comment.created_at.review_time}"
unless comment.created_at == comment.updated_at
output << " (updated on #{comment.updated_at.review_time})"
end
output << ":\n#{''.rjust(output.length + 1, "-")}\n"
output << comment.body
output << "\n\n"
}
discussion.compact.flatten unless discussion.empty?
end

# get the number of comments, including comments on commits
def comments_count(request)
#TODO
return 'N/A'
issue_c = request.comments + request.review_comments
commits_c = client.pull_commits(source_repo, request.number).
inject(0) { |sum, c| sum + c.commit.comment_count }
issue_c + commits_c
end

# show discussion for a request
def discussion(number)
#TODO
return ['Comments not available yet from API']
commit_discussion(number) +
issue_discussion(number)
end

# show latest pull request number
def latest_request_number(repo=source_repo)
requests(repo).collect(&:number).sort.last.to_i
end

# FIXME: Remove this method after merging create_pull_request from commands.rb, currently no specs
def request_url_for(target_repo, request_number)
"https://#{gitlab_host}/#{target_repo}/merge_requests/#{request_number}"
end

# FIXME: Needs to be moved into Server class, as its result is dependent of
# the actual provider (i.e. GitHub or BitBucket).
def remote_url_for(user_name, repo_name = repo_info_from_config.last)
"git@#{gitlab_host}:#{user_name}/#{repo_name}.git"
end

private

def gitlab_host
url = local.config['remote.origin.url']
url = case url
when /^http/
url
when /.*@.*:.*/
"ssh://#{url.gsub(':','/')}"
else
raise "Unable to parse gitlab host from #{url}"
end
URI.parse(url).host
end

def token_key
"gitlab_#{gitlab_host}_token"
end

def configure_token
puts "Requesting a Gitlab private token for git-review."
puts "This procedure will grant access to your public and private "\
"repositories."
puts "You can get and revoke this token by visiting the following page: "\
"https://#{gitlab_host}/profile/account"
print "Please enter your Gitlab's private token: "
settings[token_key] = STDIN.gets.chomp
print "\n"
settings.save!
puts "Private token successfully saved.\n"
end

# extract user and project name from Gitlab URL.
def url_matching(url)
matches = /#{gitlab_host}.(.*?)\/(.*)/.match(url)
matches ? [matches[1], matches[2].sub(/\.git\z/, '')] : [nil, nil]
end

private

class ClientItems
include Enumerable

def initialize(client, method, *args)
@client = client
@method = method
@args = args
end

def each
page = 1
until (items = @client.send(@method, *paginate_args(page))).empty?
page += 1
items.each { |item| yield item }
end
end

def paginate_args(page)
options = @method == :get ? {:query => { :per_page => 100, :page => page }} : { :per_page => 100, :page => page }
case @args.last
when Hash
options.merge!(@args.last)
@args[0..-2] + [options]
else
@args + [options]
end
end
end

def project_id(full_name)
settings_key = "gitlab_project_#{gitlab_host}_#{full_name.gsub('/','_')}"
return settings[settings_key] if settings[settings_key]
project = ClientItems.new(client, :projects).select do |p|
p.path_with_namespace == full_name
end.first

if project
settings[settings_key] = project.id
settings.save!
else
raise "Unknown project in Gitlab: #{full_name}"
end

project.id
end

def build_repository(project)
if project.forked_from_project
ForkedRepository.new(
:parent => { :full_name => project.forked_from_project.path_with_namespace } ,
:full_name => project.path_with_namespace
)
else
Repository.new(
:full_name => project.path_with_namespace
)
end
end

def build_request(attributes, repo)
begin
branch_info = client.branch(attributes.source_project_id, attributes.source_branch)
project_info = client.project(attributes.source_project_id)
commit_info = {
:sha => branch_info.commit.id,
:ref => branch_info.name,
:label => branch_info.name,
:user => { :login => project_info.namespace.path },
:repo => { :full_name => project_info.path_with_namespace }
}
rescue ::Gitlab::Error::NotFound
commit_info ||= {}
end
url = request_url_for(repo, attributes.iid)
Request.from_gitlab(server, attributes, commit_info, url)
end
end


end

end

# Gitlab specific constructor for git-review's request model.
class Request

# Create a new request instance from a GitHub-structured attributes hash.
def self.from_gitlab(server, request, commit_info, url)
self.new(
server: server,
number: request.iid,
title: request.title,
body: '',
head: commit_info,
state: self.state_from_gitlab(request.state),
updated_at: Time.new(request.updated_at),
html_url: url
)
end

def self.state_from_gitlab(gitlab_state)
case gitlab_state
when 'opened', 'reopened'
'open'
when 'merged', 'closed'
'closed'
else
gitlab_state
end
end

end
6 changes: 6 additions & 0 deletions lib/git-review/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def init_provider
GitReview::Provider::Bitbucket.new self
when github_provider?
GitReview::Provider::Github.new self
when gitlab_provider?
GitReview::Provider::Gitlab.new self
else
raise ::GitReview::InvalidGitProviderError
end
Expand All @@ -49,6 +51,10 @@ def bitbucket_provider?
origin_url =~ %r(bitbucket)
end

def gitlab_provider?
origin_url =~ %r(gitlab)
end

def origin_url
git_call 'config --get remote.origin.url'
end
Expand Down
Loading