2018-11-20 20:47:30 +05:30
# frozen_string_literal: true
2015-04-26 12:48:37 +05:30
class JiraService < IssueTrackerService
2017-09-10 17:25:29 +05:30
include Gitlab :: Routing
2018-03-27 19:54:05 +05:30
include ApplicationHelper
include ActionView :: Helpers :: AssetUrlHelper
2015-04-26 12:48:37 +05:30
2018-11-08 19:23:39 +05:30
validates :url , public_url : true , presence : true , if : :activated?
validates :api_url , public_url : true , allow_blank : true
2018-03-17 18:26:18 +05:30
validates :username , presence : true , if : :activated?
validates :password , presence : true , if : :activated?
2015-12-23 02:04:40 +05:30
2018-11-18 11:00:15 +05:30
validates :jira_issue_transition_id ,
format : { with : Gitlab :: Regex . jira_transition_id_regex , message : " transition ids can have only numbers which can be split with , or ; " } ,
allow_blank : true
2018-12-13 13:39:08 +05:30
# JIRA cloud version is deprecating authentication via username and password.
# We should use username/password for JIRA server and email/api_token for JIRA cloud,
# for more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/49936.
2017-09-10 17:25:29 +05:30
prop_accessor :username , :password , :url , :api_url , :jira_issue_transition_id , :title , :description
2015-12-23 02:04:40 +05:30
before_update :reset_password
2018-03-27 19:54:05 +05:30
alias_method :project_url , :url
2018-05-09 12:01:36 +05:30
# When these are false GitLab does not create cross reference
# comments on JIRA except when an issue gets transitioned.
2017-08-17 22:00:37 +05:30
def self . supported_events
%w( commit merge_request )
end
2016-11-03 12:29:30 +05:30
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
2017-09-10 17:25:29 +05:30
def self . reference_pattern ( only_long : true )
2018-03-17 18:26:18 +05:30
@reference_pattern || = / (?<issue> \ b([A-Z][A-Z0-9_]+-) \ d+) /
2016-11-03 12:29:30 +05:30
end
2017-08-17 22:00:37 +05:30
def initialize_properties
super do
self . properties = {
title : issues_tracker [ 'title' ] ,
2017-09-10 17:25:29 +05:30
url : issues_tracker [ 'url' ] ,
api_url : issues_tracker [ 'api_url' ]
2017-08-17 22:00:37 +05:30
}
end
end
2015-12-23 02:04:40 +05:30
def reset_password
2017-09-10 17:25:29 +05:30
self . password = nil if reset_password?
2015-12-23 02:04:40 +05:30
end
2015-04-26 12:48:37 +05:30
2017-08-17 22:00:37 +05:30
def options
2017-09-10 17:25:29 +05:30
url = URI . parse ( client_url )
2017-08-17 22:00:37 +05:30
{
username : self . username ,
password : self . password ,
2019-02-15 15:39:39 +05:30
site : URI . join ( url , '/' ) . to_s , # Intended to find the root
2018-03-17 18:26:18 +05:30
context_path : url . path . chomp ( '/' ) ,
2017-08-17 22:00:37 +05:30
auth_type : :basic ,
read_timeout : 120 ,
2018-03-17 18:26:18 +05:30
use_cookies : true ,
additional_cookies : [ 'OBBasicAuth=fromDialog' ] ,
2017-08-17 22:00:37 +05:30
use_ssl : url . scheme == 'https'
}
end
def client
@client || = JIRA :: Client . new ( options )
end
2015-04-26 12:48:37 +05:30
def help
2017-08-17 22:00:37 +05:30
" You need to configure JIRA before enabling this service. For more details
read the
[ JIRA service documentation ] ( #{help_page_url('user/project/integrations/jira')})."
2015-04-26 12:48:37 +05:30
end
def title
if self . properties && self . properties [ 'title' ] . present?
self . properties [ 'title' ]
else
'JIRA'
end
end
def description
if self . properties && self . properties [ 'description' ] . present?
self . properties [ 'description' ]
else
'Jira issue tracker'
end
end
2017-08-17 22:00:37 +05:30
def self . to_param
2015-04-26 12:48:37 +05:30
'jira'
end
2015-12-23 02:04:40 +05:30
def fields
2017-08-17 22:00:37 +05:30
[
2017-09-10 17:25:29 +05:30
{ type : 'text' , name : 'url' , title : 'Web URL' , placeholder : 'https://jira.example.com' , required : true } ,
{ type : 'text' , name : 'api_url' , title : 'JIRA API URL' , placeholder : 'If different from Web URL' } ,
2018-12-13 13:39:08 +05:30
{ type : 'text' , name : 'username' , title : 'Username or Email' , placeholder : 'Use a username for server version and an email for cloud version' , required : true } ,
{ type : 'password' , name : 'password' , title : 'Password or API token' , placeholder : 'Use a password for server version and an API token for cloud version' , required : true } ,
2018-11-18 11:00:15 +05:30
{ type : 'text' , name : 'jira_issue_transition_id' , title : 'Transition ID(s)' , placeholder : 'Use , or ; to separate multiple transition IDs' }
2017-08-17 22:00:37 +05:30
]
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def issues_url
" #{ url } /browse/:id "
end
def new_issue_url
" #{ url } /secure/CreateIssue.jspa "
end
def execute ( push )
# This method is a no-op, because currently JiraService does not
# support any events.
end
def close_issue ( entity , external_issue )
issue = jira_request { client . Issue . find ( external_issue . iid ) }
2017-09-10 17:25:29 +05:30
return if issue . nil? || has_resolution? ( issue ) || ! jira_issue_transition_id . present?
2017-08-17 22:00:37 +05:30
commit_id = if entity . is_a? ( Commit )
entity . id
elsif entity . is_a? ( MergeRequest )
entity . diff_head_sha
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. Refresh the issue after transition and check
# if it is closed, so we don't have one comment for every commit.
issue = jira_request { client . Issue . find ( issue . key ) } if transition_issue ( issue )
2017-09-10 17:25:29 +05:30
add_issue_solved_comment ( issue , commit_id , commit_url ) if has_resolution? ( issue )
2015-12-23 02:04:40 +05:30
end
def create_cross_reference_note ( mentioned , noteable , author )
2017-08-17 22:00:37 +05:30
unless can_cross_reference? ( noteable )
return " Events for #{ noteable . model_name . plural . humanize ( capitalize : false ) } are disabled. "
end
jira_issue = jira_request { client . Issue . find ( mentioned . id ) }
return unless jira_issue . present?
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
noteable_id = noteable . respond_to? ( :iid ) ? noteable . iid : noteable . id
noteable_type = noteable_name ( noteable )
entity_url = build_entity_url ( noteable_type , noteable_id )
2015-12-23 02:04:40 +05:30
data = {
user : {
name : author . name ,
2017-09-10 17:25:29 +05:30
url : resource_url ( user_path ( author ) )
2015-12-23 02:04:40 +05:30
} ,
project : {
2017-09-10 17:25:29 +05:30
name : project . full_path ,
url : resource_url ( namespace_project_path ( project . namespace , project ) ) # rubocop:disable Cop/ProjectPathHelper
2015-12-23 02:04:40 +05:30
} ,
entity : {
2017-08-17 22:00:37 +05:30
name : noteable_type . humanize . downcase ,
2016-06-02 11:05:42 +05:30
url : entity_url ,
title : noteable . title
2015-12-23 02:04:40 +05:30
}
}
2017-08-17 22:00:37 +05:30
add_comment ( data , jira_issue )
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def test ( _ )
result = test_settings
2017-09-10 17:25:29 +05:30
success = result . present?
result = @error if @error && ! success
2015-12-23 02:04:40 +05:30
2017-09-10 17:25:29 +05:30
{ success : success , result : result }
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
# JIRA does not need test data.
# We are requesting the project that belongs to the project key.
def test_data ( user = nil , project = nil )
nil
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def test_settings
2017-09-10 17:25:29 +05:30
return unless client_url . present?
2018-03-17 18:26:18 +05:30
2017-08-17 22:00:37 +05:30
# Test settings by getting the project
2017-09-10 17:25:29 +05:30
jira_request { client . ServerInfo . all . attrs }
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
private
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
def can_cross_reference? ( noteable )
case noteable
when Commit then commit_events
when MergeRequest then merge_requests_events
else true
end
2015-12-23 02:04:40 +05:30
end
2018-11-18 11:00:15 +05:30
# jira_issue_transition_id can have multiple values split by , or ;
# the issue is transitioned at the order given by the user
# if any transition fails it will log the error message and stop the transition sequence
2015-12-23 02:04:40 +05:30
def transition_issue ( issue )
2018-11-18 11:00:15 +05:30
jira_issue_transition_id . scan ( Gitlab :: Regex . jira_transition_id_regex ) . each do | transition_id |
2019-07-07 11:18:12 +05:30
issue . transitions . build . save! ( transition : { id : transition_id } )
rescue = > error
log_error ( " Issue transition failed " , error : error . message , client_url : client_url )
return false
2018-11-18 11:00:15 +05:30
end
2015-12-23 02:04:40 +05:30
end
def add_issue_solved_comment ( issue , commit_id , commit_url )
2019-07-07 11:18:12 +05:30
link_title = " Solved by commit #{ commit_id } . "
2017-08-17 22:00:37 +05:30
comment = " Issue solved with [ #{ commit_id } | #{ commit_url } ]. "
link_props = build_remote_link_props ( url : commit_url , title : link_title , resolved : true )
send_message ( issue , comment , link_props )
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def add_comment ( data , issue )
user_name = data [ :user ] [ :name ]
user_url = data [ :user ] [ :url ]
entity_name = data [ :entity ] [ :name ]
entity_url = data [ :entity ] [ :url ]
2016-06-02 11:05:42 +05:30
entity_title = data [ :entity ] [ :title ]
2015-12-23 02:04:40 +05:30
project_name = data [ :project ] [ :name ]
2017-08-17 22:00:37 +05:30
message = " [ #{ user_name } | #{ user_url } ] mentioned this issue in [a #{ entity_name } of #{ project_name } | #{ entity_url } ]: \n ' #{ entity_title . chomp } ' "
2019-07-07 11:18:12 +05:30
link_title = " #{ entity_name . capitalize } - #{ entity_title } "
2017-08-17 22:00:37 +05:30
link_props = build_remote_link_props ( url : entity_url , title : link_title )
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
unless comment_exists? ( issue , message )
send_message ( issue , message , link_props )
2015-12-23 02:04:40 +05:30
end
end
2017-09-10 17:25:29 +05:30
def has_resolution? ( issue )
issue . respond_to? ( :resolution ) && issue . resolution . present?
end
2017-08-17 22:00:37 +05:30
def comment_exists? ( issue , message )
comments = jira_request { issue . comments }
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
comments . present? && comments . any? { | comment | comment . body . include? ( message ) }
end
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
def send_message ( issue , message , remote_link_props )
2017-09-10 17:25:29 +05:30
return unless client_url . present?
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
jira_request do
2017-09-10 17:25:29 +05:30
remote_link = find_remote_link ( issue , remote_link_props [ :object ] [ :url ] )
if remote_link
2017-08-17 22:00:37 +05:30
remote_link . save! ( remote_link_props )
2017-09-10 17:25:29 +05:30
elsif issue . comments . build . save! ( body : message )
new_remote_link = issue . remotelink . build
new_remote_link . save! ( remote_link_props )
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
2018-11-20 20:47:30 +05:30
log_info ( " Successfully posted " , client_url : client_url )
" SUCCESS: Successfully posted to http://jira.example.net. "
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
end
2015-12-23 02:04:40 +05:30
2017-09-10 17:25:29 +05:30
def find_remote_link ( issue , url )
links = jira_request { issue . remotelink . all }
links . find { | link | link . object [ " url " ] == url }
end
2017-08-17 22:00:37 +05:30
def build_remote_link_props ( url : , title : , resolved : false )
status = {
resolved : resolved
}
{
GlobalID : 'GitLab' ,
2019-07-07 11:18:12 +05:30
relationship : 'mentioned on' ,
2017-08-17 22:00:37 +05:30
object : {
url : url ,
title : title ,
status : status ,
2018-03-27 19:54:05 +05:30
icon : {
2018-11-08 19:23:39 +05:30
title : 'GitLab' , url16x16 : asset_url ( Gitlab :: Favicon . main , host : gitlab_config . url )
2018-03-27 19:54:05 +05:30
}
2017-08-17 22:00:37 +05:30
}
}
2015-12-23 02:04:40 +05:30
end
def resource_url ( resource )
2017-08-17 22:00:37 +05:30
" #{ Settings . gitlab . base_url . chomp ( " / " ) } #{ resource } "
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def build_entity_url ( noteable_type , entity_id )
polymorphic_url (
[
self . project . namespace . becomes ( Namespace ) ,
self . project ,
noteable_type . to_sym
] ,
id : entity_id ,
host : Settings . gitlab . base_url
)
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def noteable_name ( noteable )
name = noteable . model_name . singular
# ProjectSnippet inherits from Snippet class so it causes
# routing error building the URL.
name == " project_snippet " ? " snippet " : name
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
# Handle errors when doing JIRA API calls
def jira_request
yield
2017-09-10 17:25:29 +05:30
rescue Timeout :: Error , Errno :: EINVAL , Errno :: ECONNRESET , Errno :: ECONNREFUSED , URI :: InvalidURIError , JIRA :: HTTPError , OpenSSL :: SSL :: SSLError = > e
@error = e . message
2018-11-20 20:47:30 +05:30
log_error ( " Error sending message " , client_url : client_url , error : @error )
2017-08-17 22:00:37 +05:30
nil
2015-12-23 02:04:40 +05:30
end
2017-09-10 17:25:29 +05:30
def client_url
api_url . present? ? api_url : url
end
def reset_password?
# don't reset the password if a new one is provided
return false if password_touched?
return true if api_url_changed?
return false if api_url . present?
url_changed?
end
2018-05-09 12:01:36 +05:30
def self . event_description ( event )
case event
when " merge_request " , " merge_request_events "
" JIRA comments will be created when an issue gets referenced in a merge request. "
when " commit " , " commit_events "
" JIRA comments will be created when an issue gets referenced in a commit. "
end
end
2015-04-26 12:48:37 +05:30
end