diff options
Diffstat (limited to 'app/models')
| -rw-r--r-- | app/models/application_setting.rb | 4 | ||||
| -rw-r--r-- | app/models/ci/build.rb | 13 | ||||
| -rw-r--r-- | app/models/concerns/participable.rb | 2 | ||||
| -rw-r--r-- | app/models/concerns/token_authenticatable.rb | 20 | ||||
| -rw-r--r-- | app/models/issue.rb | 2 | ||||
| -rw-r--r-- | app/models/jira_issue.rb | 2 | ||||
| -rw-r--r-- | app/models/merge_request.rb | 2 | ||||
| -rw-r--r-- | app/models/project.rb | 8 | ||||
| -rw-r--r-- | app/models/project_services/gitlab_ci_service.rb | 7 | ||||
| -rw-r--r-- | app/models/project_services/jira_service.rb | 241 | ||||
| -rw-r--r-- | app/models/user.rb | 1 |
11 files changed, 289 insertions, 13 deletions
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 1f4e8b3ef24..724429e7558 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -134,4 +134,8 @@ class ApplicationSetting < ActiveRecord::Base /x) self.restricted_signup_domains.reject! { |d| d.empty? } end + + def runners_registration_token + ensure_runners_registration_token! + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 6d9cdb95295..7b89fe069ea 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -135,6 +135,16 @@ module Ci predefined_variables + yaml_variables + project_variables + trigger_variables end + def merge_request + merge_requests = MergeRequest.includes(:merge_request_diff) + .where(source_branch: ref, source_project_id: commit.gl_project_id) + .reorder(iid: :asc) + + merge_requests.find do |merge_request| + merge_request.commits.any? { |ci| ci.id == commit.sha } + end + end + def project commit.project end @@ -170,7 +180,8 @@ module Ci def extract_coverage(text, regex) begin - matches = text.gsub(Regexp.new(regex)).to_a.last + matches = text.scan(Regexp.new(regex)).last + matches = matches.last if matches.kind_of?(Array) coverage = matches.gsub(/\d+(\.\d+)?/).first if coverage.present? diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 808d80b0530..fc6f83b918b 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -37,7 +37,7 @@ module Participable # Be aware that this method makes a lot of sql queries. # Save result into variable if you are going to reuse it inside same request - def participants(current_user = self.author, load_lazy_references: true) + def participants(current_user = self.author) participants = Gitlab::ReferenceExtractor.lazily do self.class.participant_attrs.flat_map do |attr| diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 488ff8c31b7..885deaf78d2 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -18,15 +18,16 @@ module TokenAuthenticatable define_method("ensure_#{token_field}") do current_token = read_attribute(token_field) - if current_token.blank? - write_attribute(token_field, generate_token_for(token_field)) - else - current_token - end + current_token.blank? ? write_new_token(token_field) : current_token + end + + define_method("ensure_#{token_field}!") do + send("reset_#{token_field}!") if read_attribute(token_field).blank? + read_attribute(token_field) end define_method("reset_#{token_field}!") do - write_attribute(token_field, generate_token_for(token_field)) + write_new_token(token_field) save! end end @@ -34,7 +35,12 @@ module TokenAuthenticatable private - def generate_token_for(token_field) + def write_new_token(token_field) + new_token = generate_token(token_field) + write_attribute(token_field, new_token) + end + + def generate_token(token_field) loop do token = Devise.friendly_token break token unless self.class.unscoped.find_by(token_field => token) diff --git a/app/models/issue.rb b/app/models/issue.rb index 4571d7f0ee1..80ecd15077f 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -86,7 +86,7 @@ class Issue < ActiveRecord::Base def referenced_merge_requests Gitlab::ReferenceExtractor.lazily do [self, *notes].flat_map do |note| - note.all_references(load_lazy_references: false).merge_requests + note.all_references.merge_requests end end.sort_by(&:iid) end diff --git a/app/models/jira_issue.rb b/app/models/jira_issue.rb new file mode 100644 index 00000000000..5b21aac5e43 --- /dev/null +++ b/app/models/jira_issue.rb @@ -0,0 +1,2 @@ +class JiraIssue < ExternalIssue +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index d7430d36c41..ac25d38eb63 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -335,7 +335,7 @@ class MergeRequest < ActiveRecord::Base issues = commits.flat_map { |c| c.closes_issues(current_user) } issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user). closed_by_message(description)) - issues.uniq + issues.uniq(&:id) else [] end diff --git a/app/models/project.rb b/app/models/project.rb index 13fd383237c..b28a7ca429c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -499,6 +499,10 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.find(&:activated?) end + def jira_tracker? + issues_tracker.to_param == 'jira' + end + def avatar_type unless self.avatar.image? self.errors.add :avatar, 'only images allowed' @@ -799,6 +803,10 @@ class Project < ActiveRecord::Base false end + def jira_tracker_active? + jira_tracker? && jira_service.active + end + def ci_commit(sha) ci_commits.find_by(sha: sha) end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index d73182d40ac..b64d97ce75d 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -18,6 +18,11 @@ # note_events :boolean default(TRUE), not null # +# TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed class GitlabCiService < CiService - # this is no longer used + # We override the active accessor to always make GitLabCiService disabled + # Otherwise the GitLabCiService can be picked, but should never be since it's deprecated + def active + false + end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 35e30b1cb0b..e216f406e1c 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -19,9 +19,24 @@ # class JiraService < IssueTrackerService + include HTTParty include Gitlab::Application.routes.url_helpers - prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + DEFAULT_API_VERSION = 2 + + prop_accessor :username, :password, :api_url, :jira_issue_transition_id, + :title, :description, :project_url, :issues_url, :new_issue_url + + before_validation :set_api_url, :set_jira_issue_transition_id + + before_update :reset_password + + def reset_password + # don't reset the password if a new one is provided + if api_url_changed? && !password_touched? + self.password = nil + end + end def help line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\ @@ -54,4 +69,228 @@ class JiraService < IssueTrackerService def to_param 'jira' end + + def fields + super.push( + { type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' }, + { type: 'text', name: 'username', placeholder: '' }, + { type: 'password', name: 'password', placeholder: '' }, + { type: 'text', name: 'jira_issue_transition_id', placeholder: '2' } + ) + end + + def execute(push, issue = nil) + if issue.nil? + # No specific issue, that means + # we just want to test settings + test_settings + else + close_issue(push, issue) + end + end + + def create_cross_reference_note(mentioned, noteable, author) + issue_name = mentioned.id + project = self.project + noteable_name = noteable.class.name.underscore.downcase + noteable_id = if noteable.is_a?(Commit) + noteable.id + else + noteable.iid + end + + entity_url = build_entity_url(noteable_name.to_sym, noteable_id) + + data = { + user: { + name: author.name, + url: resource_url(user_path(author)), + }, + project: { + name: project.path_with_namespace, + url: resource_url(namespace_project_path(project.namespace, project)) + }, + entity: { + name: noteable_name.humanize.downcase, + url: entity_url + } + } + + add_comment(data, issue_name) + end + + def test_settings + result = JiraService.get( + jira_api_test_url, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{auth}" + } + ) + + case result.code + when 201, 200 + Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Successfully connected to #{api_url}.") + true + else + Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}") + false + end + rescue Errno::ECONNREFUSED => e + Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}." + false + end + + private + + def build_api_url_from_project_url + server = URI(project_url) + default_ports = [["http",80],["https",443]].include?([server.scheme,server.port]) + server_url = "#{server.scheme}://#{server.host}" + server_url.concat(":#{server.port}") unless default_ports + "#{server_url}/rest/api/#{DEFAULT_API_VERSION}" + rescue + "" # looks like project URL was not valid + end + + def set_api_url + self.api_url = build_api_url_from_project_url if self.api_url.blank? + end + + def set_jira_issue_transition_id + self.jira_issue_transition_id ||= "2" + end + + def close_issue(entity, issue) + commit_id = if entity.is_a?(Commit) + entity.id + elsif entity.is_a?(MergeRequest) + entity.last_commit.id + end + commit_url = build_entity_url(:commit, commit_id) + + # Depending on the JIRA project's workflow, a comment during transition + # may or may not be allowed. Split the operation in to two calls so the + # comment always works. + transition_issue(issue) + add_issue_solved_comment(issue, commit_id, commit_url) + end + + def transition_issue(issue) + message = { + transition: { + id: jira_issue_transition_id + } + } + send_message(close_issue_url(issue.iid), message.to_json) + end + + def add_issue_solved_comment(issue, commit_id, commit_url) + comment = { + body: "Issue solved with [#{commit_id}|#{commit_url}]." + } + + send_message(comment_url(issue.iid), comment.to_json) + end + + def add_comment(data, issue_name) + url = comment_url(issue_name) + user_name = data[:user][:name] + user_url = data[:user][:url] + entity_name = data[:entity][:name] + entity_url = data[:entity][:url] + project_name = data[:project][:name] + + message = { + body: "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]." + } + + unless existing_comment?(issue_name, message[:body]) + send_message(url, message.to_json) + end + end + + + def auth + require 'base64' + Base64.urlsafe_encode64("#{self.username}:#{self.password}") + end + + def send_message(url, message) + result = JiraService.post( + url, + body: message, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{auth}" + } + ) + + message = case result.code + when 201, 200, 204 + "#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}." + when 401 + "#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again." + else + "#{self.class.name} ERROR #{result.code}: #{result.parsed_response}" + end + + Rails.logger.info(message) + message + rescue URI::InvalidURIError, Errno::ECONNREFUSED => e + Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{url}." + end + + def existing_comment?(issue_name, new_comment) + result = JiraService.get( + comment_url(issue_name), + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{auth}" + } + ) + + case result.code + when 201, 200 + existing_comments = JSON.parse(result.body)['comments'] + + if existing_comments.present? + return existing_comments.map { |comment| comment['body'].include?(new_comment) }.any? + end + end + + false + rescue JSON::ParserError + false + end + + def resource_url(resource) + "#{Settings.gitlab['url'].chomp("/")}#{resource}" + end + + def build_entity_url(entity_name, entity_id) + resource_url( + polymorphic_url( + [ + self.project.namespace.becomes(Namespace), + self.project, + entity_name + ], + id: entity_id, + routing_type: :path + ) + ) + end + + def close_issue_url(issue_name) + "#{self.api_url}/issue/#{issue_name}/transitions" + end + + def comment_url(issue_name) + "#{self.api_url}/issue/#{issue_name}/comment" + end + + def jira_api_test_url + "#{self.api_url}/myself" + end end diff --git a/app/models/user.rb b/app/models/user.rb index e0ce091c54e..df87f3b79bd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,6 +26,7 @@ # bio :string(255) # failed_attempts :integer default(0) # locked_at :datetime +# unlock_token :string(255) # username :string(255) # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null |
