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 ,
2019-07-31 22:56:46 +05:30
format : { with : Gitlab :: Regex . jira_transition_id_regex , message : s_ ( " JiraService|transition ids can have only numbers which can be split with , or ; " ) } ,
2018-11-18 11:00:15 +05:30
allow_blank : true
2019-09-30 21:07:59 +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,
2019-12-04 20:38:33 +05:30
# for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936.
# TODO: we can probably just delegate as part of
2019-12-21 20:55:43 +05:30
# https://gitlab.com/gitlab-org/gitlab/issues/29404
2019-12-04 20:38:33 +05:30
data_field :username , :password , :url , :api_url , :jira_issue_transition_id
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
2019-09-30 21:07:59 +05:30
# 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
2020-01-01 13:55:28 +05:30
def self . supported_event_actions
%w( comment )
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 )
2019-10-12 21:52:04 +05:30
@reference_pattern || = / (?<issue> \ b #{ Gitlab :: Regex . jira_issue_key_regex } ) /
2016-11-03 12:29:30 +05:30
end
2017-08-17 22:00:37 +05:30
def initialize_properties
2019-12-04 20:38:33 +05:30
{ }
end
def data_fields
jira_tracker_data || self . build_jira_tracker_data
2017-08-17 22:00:37 +05:30
end
2015-12-23 02:04:40 +05:30
def reset_password
2019-12-04 20:38:33 +05:30
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-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
{
2019-12-21 20:55:43 +05:30
username : username & . strip ,
2019-12-04 20:38:33 +05:30
password : password ,
2019-02-15 15:39:39 +05:30
site : URI . join ( url , '/' ) . to_s , # Intended to find the root
2019-10-12 21:52:04 +05:30
context_path : url . path ,
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
2019-09-04 21:01:54 +05:30
@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
2017-08-17 22:00:37 +05:30
end
2015-04-26 12:48:37 +05:30
def help
2019-09-30 21:07:59 +05:30
" You need to configure Jira before enabling this service. For more details
2017-08-17 22:00:37 +05:30
read the
2019-09-30 21:07:59 +05:30
[ Jira service documentation ] ( #{help_page_url('user/project/integrations/jira')})."
2015-04-26 12:48:37 +05:30
end
2019-09-30 21:07:59 +05:30
def default_title
'Jira'
2015-04-26 12:48:37 +05:30
end
2019-09-30 21:07:59 +05:30
def default_description
s_ ( 'JiraService|Jira issue tracker' )
2015-04-26 12:48:37 +05:30
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
[
2019-07-31 22:56:46 +05:30
{ type : 'text' , name : 'url' , title : s_ ( 'JiraService|Web URL' ) , placeholder : 'https://jira.example.com' , required : true } ,
2019-09-30 21:07:59 +05:30
{ type : 'text' , name : 'api_url' , title : s_ ( 'JiraService|Jira API URL' ) , placeholder : s_ ( 'JiraService|If different from Web URL' ) } ,
2019-07-31 22:56:46 +05:30
{ 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' ) }
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
2019-10-12 21:52:04 +05:30
alias_method :original_url , :url
def url
2019-12-21 20:55:43 +05:30
original_url & . delete_suffix ( '/' )
end
alias_method :original_api_url , :api_url
def api_url
original_api_url & . delete_suffix ( '/' )
2019-10-12 21:52:04 +05:30
end
2017-08-17 22:00:37 +05:30
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
2019-12-21 20:55:43 +05:30
commit_id = case entity
when Commit then entity . id
when MergeRequest then entity . diff_head_sha
2017-08-17 22:00:37 +05:30
end
commit_url = build_entity_url ( :commit , commit_id )
2019-09-30 21:07:59 +05:30
# Depending on the Jira project's workflow, a comment during transition
2017-08-17 22:00:37 +05:30
# 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 )
2019-07-31 22:56:46 +05:30
return s_ ( " JiraService|Events for %{noteable_model_name} are disabled. " ) % { noteable_model_name : noteable . model_name . plural . humanize ( capitalize : false ) }
2017-08-17 22:00:37 +05:30
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
2019-09-30 21:07:59 +05:30
# Jira does not need test data.
2017-08-17 22:00:37 +05:30
# 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
2020-01-01 13:55:28 +05:30
create_issue_link ( issue , remote_link_props )
create_issue_comment ( issue , message )
2017-08-17 22:00:37 +05:30
2018-11-20 20:47:30 +05:30
log_info ( " Successfully posted " , client_url : client_url )
2019-09-30 21:07:59 +05:30
" SUCCESS: Successfully posted to #{ client_url } . "
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
2020-01-01 13:55:28 +05:30
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-09-10 17:25:29 +05:30
def find_remote_link ( issue , url )
links = jira_request { issue . remotelink . all }
2019-09-04 21:01:54 +05:30
return unless links
2017-09-10 17:25:29 +05:30
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 : {
2019-12-21 20:55:43 +05:30
title : 'GitLab' , url16x16 : asset_url ( Gitlab :: Favicon . main , host : gitlab_config . base_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
2019-09-30 21:07:59 +05:30
# Handle errors when doing Jira API calls
2017-08-17 22:00:37 +05:30
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
2019-12-21 20:55:43 +05:30
api_url . presence || url
2017-09-10 17:25:29 +05:30
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 "
2019-09-30 21:07:59 +05:30
s_ ( " JiraService|Jira comments will be created when an issue gets referenced in a merge request. " )
2018-05-09 12:01:36 +05:30
when " commit " , " commit_events "
2019-09-30 21:07:59 +05:30
s_ ( " JiraService|Jira comments will be created when an issue gets referenced in a commit. " )
2018-05-09 12:01:36 +05:30
end
end
2015-04-26 12:48:37 +05:30
end