-
Notifications
You must be signed in to change notification settings - Fork 16
Gitlab integration development #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
brugidou
wants to merge
15
commits into
b4mboo:development
Choose a base branch
from
criteo-forks:gitlab-dev
base: development
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
2c154ff
Initial Gitlab provider implementation
brugidou 8ae92a2
Don't use search to get project_id
brugidou 6ae02e9
API /projects/all is for admin only
brugidou 776987d
Update using the from_gitlab pattern
brugidou 7b65339
Remove unnecessary check for Request class
brugidou 70489ad
Paginate through all gitlab objects
brugidou e109bc1
Add simple spec for now
brugidou 5b99dec
[gitlab] use internal id for all merge requests
brugidou 3a7bf89
[gitlab] Fix pull_request method
brugidou d08cd6d
Gitlab: Fix the review post-creation step
brugidou 997a398
Add ability to close reviews + add description
brugidou e68c3fe
Add reopened state for gitlab and fix fallback
brugidou dc696cd
Fixes to catch up with develop branch
brugidou b8f2169
Only get open merge requests in gitlab
brugidou 83386a1
Use gitlab MR updated_at field
brugidou File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 ingithub.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:
requestmethod doesn't return an instance ofRequest.commit_discussionwill no longer be reachable, as it is implemented inbase.rbnow.commits, which is supposed to be querying the providers API and create a collection ofCommitinstances.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?