2018-08-10 02:45:01 -04:00
# frozen_string_literal: true
2015-01-20 19:46:27 -05:00
class JiraService < IssueTrackerService
2017-07-07 11:43:37 -04:00
include Gitlab :: Routing
2018-03-05 14:32:51 -05:00
include ApplicationHelper
include ActionView :: Helpers :: AssetUrlHelper
2015-01-20 19:46:27 -05:00
2018-06-01 07:43:53 -04:00
validates :url , public_url : true , presence : true , if : :activated?
validates :api_url , public_url : true , allow_blank : true
2017-10-24 03:35:21 -04:00
validates :username , presence : true , if : :activated?
validates :password , presence : true , if : :activated?
2015-12-17 17:08:14 -05:00
2018-07-31 15:17:22 -04:00
validates :jira_issue_transition_id ,
2019-04-12 08:28:07 -04:00
format : { with : Gitlab :: Regex . jira_transition_id_regex , message : s_ ( " JiraService|transition ids can have only numbers which can be split with , or ; " ) } ,
2018-07-31 15:17:22 -04:00
allow_blank : true
2019-06-28 09:25:56 -04:00
# 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,
2019-09-18 10:02:45 -04:00
# for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936.
2019-09-16 11:06:26 -04:00
# TODO: we can probably just delegate as part of
2019-10-07 11:05:59 -04:00
# https://gitlab.com/gitlab-org/gitlab/issues/29404
2019-09-16 11:06:26 -04:00
data_field :username , :password , :url , :api_url , :jira_issue_transition_id
2016-04-21 11:13:14 -04:00
2015-12-17 17:08:14 -05:00
before_update :reset_password
2018-02-09 05:18:53 -05:00
alias_method :project_url , :url
2018-03-16 15:09:35 -04:00
# When these are false GitLab does not create cross reference
2019-06-28 09:25:56 -04:00
# comments on Jira except when an issue gets transitioned.
2016-12-27 07:44:24 -05:00
def self . supported_events
2016-11-14 16:30:01 -05:00
%w( commit merge_request )
end
2019-11-26 07:06:18 -05:00
def self . supported_event_actions
%w( comment )
end
2016-10-06 18:05:27 -04:00
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
2017-07-10 03:38:42 -04:00
def self . reference_pattern ( only_long : true )
2019-07-25 05:29:51 -04:00
@reference_pattern || = / (?<issue> \ b #{ Gitlab :: Regex . jira_issue_key_regex } ) /
2016-10-06 18:05:27 -04:00
end
2016-09-29 17:11:32 -04:00
def initialize_properties
2019-09-16 11:06:26 -04:00
{ }
end
def data_fields
jira_tracker_data || self . build_jira_tracker_data
2016-09-29 17:11:32 -04:00
end
2015-12-17 17:08:14 -05:00
def reset_password
2019-09-16 11:06:26 -04:00
data_fields . password = nil if reset_password?
end
def set_default_data
return unless issues_tracker . present?
self . title || = issues_tracker [ 'title' ]
return if url
data_fields . url || = issues_tracker [ 'url' ]
data_fields . api_url || = issues_tracker [ 'api_url' ]
2015-12-17 17:08:14 -05:00
end
2015-01-20 19:46:27 -05:00
2016-01-14 09:20:23 -05:00
def options
2017-05-23 05:14:43 -04:00
url = URI . parse ( client_url )
2016-09-29 17:11:32 -04:00
2016-01-14 09:20:23 -05:00
{
2019-10-11 08:06:29 -04:00
username : username & . strip ,
2019-09-16 11:06:26 -04:00
password : password ,
2018-11-09 18:31:26 -05:00
site : URI . join ( url , '/' ) . to_s , # Intended to find the root
2019-07-18 12:44:24 -04:00
context_path : url . path ,
2016-09-29 17:11:32 -04:00
auth_type : :basic ,
read_timeout : 120 ,
2017-12-18 06:15:41 -05:00
use_cookies : true ,
additional_cookies : [ 'OBBasicAuth=fromDialog' ] ,
2016-09-29 17:11:32 -04:00
use_ssl : url . scheme == 'https'
2016-01-14 09:20:23 -05:00
}
end
def client
2019-07-16 15:49:47 -04:00
@client || = begin
JIRA :: Client . new ( options ) . tap do | client |
# Replaces JIRA default http client with our implementation
client . request_client = Gitlab :: Jira :: HttpClient . new ( client . options )
end
end
2016-01-14 09:20:23 -05:00
end
2015-02-12 20:06:55 -05:00
def help
2019-06-28 09:25:56 -04:00
" You need to configure Jira before enabling this service. For more details
2016-11-22 08:12:09 -05:00
read the
2019-06-28 09:25:56 -04:00
[ Jira service documentation ] ( #{help_page_url('user/project/integrations/jira')})."
2015-02-12 20:06:55 -05:00
end
2019-06-26 10:03:57 -04:00
def default_title
'Jira'
2015-01-20 19:46:27 -05:00
end
2019-06-26 10:03:57 -04:00
def default_description
s_ ( 'JiraService|Jira issue tracker' )
2015-01-20 19:46:27 -05:00
end
2016-12-27 07:44:24 -05:00
def self . to_param
2015-01-20 19:46:27 -05:00
'jira'
end
2015-12-17 17:08:14 -05:00
def fields
2016-01-14 09:20:23 -05:00
[
2019-04-12 08:28:07 -04:00
{ type : 'text' , name : 'url' , title : s_ ( 'JiraService|Web URL' ) , placeholder : 'https://jira.example.com' , required : true } ,
2019-06-28 09:25:56 -04:00
{ type : 'text' , name : 'api_url' , title : s_ ( 'JiraService|Jira API URL' ) , placeholder : s_ ( 'JiraService|If different from Web URL' ) } ,
2019-04-12 08:28:07 -04:00
{ type : 'text' , name : 'username' , title : s_ ( 'JiraService|Username or Email' ) , placeholder : s_ ( 'JiraService|Use a username for server version and an email for cloud version' ) , required : true } ,
{ type : 'password' , name : 'password' , title : s_ ( 'JiraService|Password or API token' ) , placeholder : s_ ( 'JiraService|Use a password for server version and an API token for cloud version' ) , required : true } ,
{ type : 'text' , name : 'jira_issue_transition_id' , title : s_ ( 'JiraService|Transition ID(s)' ) , placeholder : s_ ( 'JiraService|Use , or ; to separate multiple transition IDs' ) }
2016-01-14 09:20:23 -05:00
]
end
def issues_url
" #{ url } /browse/:id "
end
def new_issue_url
" #{ url } /secure/CreateIssue.jspa "
2015-12-17 17:08:14 -05:00
end
2019-07-18 12:44:24 -04:00
alias_method :original_url , :url
def url
2019-10-16 17:07:22 -04:00
original_url & . delete_suffix ( '/' )
end
alias_method :original_api_url , :api_url
def api_url
original_api_url & . delete_suffix ( '/' )
2019-07-18 12:44:24 -04:00
end
2016-12-01 03:45:06 -05:00
def execute ( push )
# This method is a no-op, because currently JiraService does not
# support any events.
end
2016-11-09 16:55:21 -05:00
2016-12-01 03:45:06 -05:00
def close_issue ( entity , external_issue )
issue = jira_request { client . Issue . find ( external_issue . iid ) }
2016-11-09 16:55:21 -05:00
2017-08-01 15:19:54 -04:00
return if issue . nil? || has_resolution? ( issue ) || ! jira_issue_transition_id . present?
2016-12-01 03:45:06 -05:00
2019-09-25 08:06:15 -04:00
commit_id = case entity
when Commit then entity . id
when MergeRequest then entity . diff_head_sha
2016-12-01 03:45:06 -05:00
end
commit_url = build_entity_url ( :commit , commit_id )
2019-06-28 09:25:56 -04:00
# Depending on the Jira project's workflow, a comment during transition
2016-12-01 03:45:06 -05:00
# 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-08-01 15:19:54 -04:00
add_issue_solved_comment ( issue , commit_id , commit_url ) if has_resolution? ( issue )
2015-12-17 17:08:14 -05:00
end
def create_cross_reference_note ( mentioned , noteable , author )
2016-11-14 16:30:01 -05:00
unless can_cross_reference? ( noteable )
2019-04-12 08:28:07 -04:00
return s_ ( " JiraService|Events for %{noteable_model_name} are disabled. " ) % { noteable_model_name : noteable . model_name . plural . humanize ( capitalize : false ) }
2016-11-14 16:30:01 -05:00
end
2016-11-09 16:55:21 -05:00
jira_issue = jira_request { client . Issue . find ( mentioned . id ) }
2016-11-14 16:30:01 -05:00
return unless jira_issue . present?
2016-11-09 16:55:21 -05:00
2016-11-17 15:46:31 -05:00
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-17 17:08:14 -05:00
data = {
user : {
name : author . name ,
2017-05-03 07:22:03 -04:00
url : resource_url ( user_path ( author ) )
2015-12-17 17:08:14 -05:00
} ,
project : {
2017-07-20 05:34:09 -04:00
name : project . full_path ,
2017-06-29 13:06:35 -04:00
url : resource_url ( namespace_project_path ( project . namespace , project ) ) # rubocop:disable Cop/ProjectPathHelper
2015-12-17 17:08:14 -05:00
} ,
entity : {
2016-11-17 15:46:31 -05:00
name : noteable_type . humanize . downcase ,
2016-09-29 17:11:32 -04:00
url : entity_url ,
title : noteable . title
2015-12-17 17:08:14 -05:00
}
}
2016-11-09 16:55:21 -05:00
add_comment ( data , jira_issue )
2015-12-17 17:08:14 -05:00
end
2016-12-01 03:45:06 -05:00
def test ( _ )
result = test_settings
2017-07-26 03:57:56 -04:00
success = result . present?
result = @error if @error && ! success
{ success : success , result : result }
2016-12-01 03:45:06 -05:00
end
2019-06-28 09:25:56 -04:00
# Jira does not need test data.
2016-10-27 17:20:47 -04:00
# We are requesting the project that belongs to the project key.
def test_data ( user = nil , project = nil )
nil
end
2015-12-17 17:08:14 -05:00
def test_settings
2017-05-23 05:14:43 -04:00
return unless client_url . present?
2017-11-14 04:02:39 -05:00
2016-01-14 09:20:23 -05:00
# Test settings by getting the project
2017-07-25 11:25:41 -04:00
jira_request { client . ServerInfo . all . attrs }
2015-12-17 17:08:14 -05:00
end
private
2016-11-14 16:30:01 -05:00
def can_cross_reference? ( noteable )
case noteable
when Commit then commit_events
when MergeRequest then merge_requests_events
else true
end
end
2018-07-31 15:17:22 -04:00
# 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-17 17:08:14 -05:00
def transition_issue ( issue )
2018-07-31 15:17:22 -04:00
jira_issue_transition_id . scan ( Gitlab :: Regex . jira_transition_id_regex ) . each do | transition_id |
2019-03-13 09:42:43 -04:00
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-07-31 15:17:22 -04:00
end
2015-12-17 17:08:14 -05:00
end
def add_issue_solved_comment ( issue , commit_id , commit_url )
2019-02-28 04:46:09 -05:00
link_title = " Solved by commit #{ commit_id } . "
2016-11-09 16:55:21 -05:00
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-17 17:08:14 -05:00
end
2016-11-09 16:55:21 -05:00
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-09-29 17:11:32 -04:00
entity_title = data [ :entity ] [ :title ]
2015-12-17 17:08:14 -05:00
project_name = data [ :project ] [ :name ]
2016-11-04 15:36:37 -04:00
message = " [ #{ user_name } | #{ user_url } ] mentioned this issue in [a #{ entity_name } of #{ project_name } | #{ entity_url } ]: \n ' #{ entity_title . chomp } ' "
2019-02-28 04:46:09 -05:00
link_title = " #{ entity_name . capitalize } - #{ entity_title } "
2016-11-09 16:55:21 -05:00
link_props = build_remote_link_props ( url : entity_url , title : link_title )
2015-12-17 17:08:14 -05:00
2016-11-09 16:55:21 -05:00
unless comment_exists? ( issue , message )
send_message ( issue , message , link_props )
2016-09-29 17:11:32 -04:00
end
2015-12-17 17:08:14 -05:00
end
2017-08-01 15:19:54 -04:00
def has_resolution? ( issue )
issue . respond_to? ( :resolution ) && issue . resolution . present?
end
2016-11-09 16:55:21 -05:00
def comment_exists? ( issue , message )
comments = jira_request { issue . comments }
comments . present? && comments . any? { | comment | comment . body . include? ( message ) }
2015-12-17 17:08:14 -05:00
end
2016-11-09 16:55:21 -05:00
def send_message ( issue , message , remote_link_props )
2017-05-23 05:14:43 -04:00
return unless client_url . present?
2015-12-17 17:08:14 -05:00
2016-11-09 16:55:21 -05:00
jira_request do
2019-11-26 07:06:18 -05:00
create_issue_link ( issue , remote_link_props )
create_issue_comment ( issue , message )
2015-12-17 17:08:14 -05:00
2018-08-20 14:34:07 -04:00
log_info ( " Successfully posted " , client_url : client_url )
2019-07-16 04:04:30 -04:00
" SUCCESS: Successfully posted to #{ client_url } . "
2015-12-17 17:08:14 -05:00
end
2016-11-09 16:55:21 -05:00
end
2015-12-17 17:08:14 -05:00
2019-11-26 07:06:18 -05:00
def create_issue_link ( issue , remote_link_props )
remote_link = find_remote_link ( issue , remote_link_props [ :object ] [ :url ] )
remote_link || = issue . remotelink . build
remote_link . save! ( remote_link_props )
end
def create_issue_comment ( issue , message )
return unless comment_on_event_enabled
issue . comments . build . save! ( body : message )
end
2017-05-18 02:07:48 -04:00
def find_remote_link ( issue , url )
links = jira_request { issue . remotelink . all }
2019-05-10 09:33:44 -04:00
return unless links
2017-05-18 02:07:48 -04:00
links . find { | link | link . object [ " url " ] == url }
end
2016-11-09 16:55:21 -05:00
def build_remote_link_props ( url : , title : , resolved : false )
status = {
resolved : resolved
}
{
GlobalID : 'GitLab' ,
2019-02-28 04:46:09 -05:00
relationship : 'mentioned on' ,
2016-11-09 16:55:21 -05:00
object : {
url : url ,
title : title ,
status : status ,
2018-03-05 14:32:51 -05:00
icon : {
2019-09-18 10:02:45 -04:00
title : 'GitLab' , url16x16 : asset_url ( Gitlab :: Favicon . main , host : gitlab_config . base_url )
2018-03-05 14:32:51 -05:00
}
2016-11-09 16:55:21 -05:00
}
}
2015-12-17 17:08:14 -05:00
end
def resource_url ( resource )
2016-09-01 01:50:27 -04:00
" #{ Settings . gitlab . base_url . chomp ( " / " ) } #{ resource } "
2015-12-17 17:08:14 -05:00
end
2016-11-17 15:46:31 -05:00
def build_entity_url ( noteable_type , entity_id )
2016-11-05 09:59:08 -04:00
polymorphic_url (
[
self . project . namespace . becomes ( Namespace ) ,
self . project ,
2016-11-17 15:46:31 -05:00
noteable_type . to_sym
2016-11-05 09:59:08 -04:00
] ,
id : entity_id ,
host : Settings . gitlab . base_url
2015-12-17 17:08:14 -05:00
)
end
2016-11-09 16:55:21 -05:00
2016-11-17 15:46:31 -05:00
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
end
2019-06-28 09:25:56 -04:00
# Handle errors when doing Jira API calls
2016-11-09 16:55:21 -05:00
def jira_request
yield
2017-05-18 02:20:16 -04:00
rescue Timeout :: Error , Errno :: EINVAL , Errno :: ECONNRESET , Errno :: ECONNREFUSED , URI :: InvalidURIError , JIRA :: HTTPError , OpenSSL :: SSL :: SSLError = > e
2017-07-26 03:57:56 -04:00
@error = e . message
2018-08-20 14:34:07 -04:00
log_error ( " Error sending message " , client_url : client_url , error : @error )
2016-11-09 16:55:21 -05:00
nil
end
2017-05-23 05:14:43 -04:00
def client_url
2019-09-26 05:06:04 -04:00
api_url . presence || url
2017-05-23 05:14:43 -04:00
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-03-16 15:09:35 -04:00
def self . event_description ( event )
case event
when " merge_request " , " merge_request_events "
2019-06-28 09:25:56 -04:00
s_ ( " JiraService|Jira comments will be created when an issue gets referenced in a merge request. " )
2018-03-16 15:09:35 -04:00
when " commit " , " commit_events "
2019-06-28 09:25:56 -04:00
s_ ( " JiraService|Jira comments will be created when an issue gets referenced in a commit. " )
2018-03-16 15:09:35 -04:00
end
end
2015-01-20 19:46:27 -05:00
end