Commit ccff1051 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'jira_service' into 'master'

Close Jira issues with commit messages and merge requests
parents fcc32002 92917d89
v 6.9.0 v 6.9.0
- Support Jira ticket mentions in format JIRA-123 - Support Jira ticket mentions in format JIRA-123
- Add support for closing Jira tickets with commits and MR
v 6.8.0 v 6.8.0
- Customise sign-in page with custom text and logo - Customise sign-in page with custom text and logo
......
...@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base ...@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base
has_one :assembla_service, dependent: :destroy has_one :assembla_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy
has_one :slack_service, dependent: :destroy has_one :slack_service, dependent: :destroy
has_one :jira_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link has_one :forked_from_project, through: :forked_project_link
# Merge Requests for target project should be removed with it # Merge Requests for target project should be removed with it
...@@ -316,13 +317,17 @@ class Project < ActiveRecord::Base ...@@ -316,13 +317,17 @@ class Project < ActiveRecord::Base
end end
def available_services_names def available_services_names
%w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack) %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack jira)
end end
def gitlab_ci? def gitlab_ci?
gitlab_ci_service && gitlab_ci_service.active gitlab_ci_service && gitlab_ci_service.active
end end
def jira_tracker?
self.issues_tracker == "jira"
end
# For compatibility with old code # For compatibility with old code
def code def code
path path
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# token :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# project_url :string(255)
# subdomain :string(255)
# room :string(255)
# recipients :text
# api_key :string(255)
# username :string(255)
# password :string(255)
# api_version :string(255)
class JiraService < Service
include HTTParty
attr_accessible :project_url, :username, :password, :api_version
validates :username, :password, presence: true, if: :activated?
before_validation :set_api_version
def title
'JIRA'
end
def description
'Bug, issue tracking, and project management system'
end
def to_param
'jira'
end
def fields
[
{ type: 'text', name: 'project_url', placeholder: 'Url to JIRA, http://jira.example' },
{ type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' },
{ type: 'text', name: 'api_version', placeholder: '2' }
]
end
def set_api_version
self.api_version = "2"
end
def execute(push, issue = nil)
close_issue(push, issue) if issue
end
private
def close_issue(push_data, issue_name)
url = close_issue_url(issue_name)
commit_url = push_data[:commits].first[:url]
message = {
'update' => {
'comment' => [{
'add' => {
'body' => "Issue solved with #{commit_url}"
}
}]
},
'transition' => {
'id' => '2'
}
}
JiraService.post(
url,
body: message.to_json,
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Basic #{auth}"
}
)
end
def close_issue_url(issue_name)
"#{self.project_url.chomp("/")}/rest/api/#{self.api_version}/issue/#{issue_name}/transitions"
end
def auth
require 'base64'
Base64.urlsafe_encode64("#{self.username}:#{self.password}")
end
end
...@@ -88,7 +88,11 @@ class GitPushService ...@@ -88,7 +88,11 @@ class GitPushService
if !issues_to_close.empty? && is_default_branch if !issues_to_close.empty? && is_default_branch
issues_to_close.each do |issue| issues_to_close.each do |issue|
Issues::CloseService.new(project, author, {}).execute(issue, commit) if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(push_data, issue)
else
Issues::CloseService.new(project, author, {}).execute(issue, commit)
end
end end
end end
......
...@@ -37,6 +37,8 @@ ...@@ -37,6 +37,8 @@
= f.text_area name, rows: 5, class: "form-control", placeholder: placeholder = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder
- elsif type == 'checkbox' - elsif type == 'checkbox'
= f.check_box name = f.check_box name
- elsif type == 'password'
= f.password_field name, class: "form-control"
.form-actions .form-actions
= f.submit 'Save', class: 'btn btn-save' = f.submit 'Save', class: 'btn btn-save'
......
...@@ -74,7 +74,7 @@ production: &base ...@@ -74,7 +74,7 @@ production: &base
# If a commit message matches this regular expression, all issues referenced from the matched text will be closed. # If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
# This happens when the commit is pushed or merged into the default branch of a project. # This happens when the commit is pushed or merged into the default branch of a project.
# When not specified the default issue_closing_pattern as specified below will be used. # When not specified the default issue_closing_pattern as specified below will be used.
# issue_closing_pattern: '([Cc]lose[sd]|[Ff]ixe[sd]) #(\d+)' # issue_closing_pattern: '([Cc]lose[sd]|[Ff]ixe[sd]) (#\d+|([A-Z\-]+-)\d+)'
## Default project features settings ## Default project features settings
default_projects_features: default_projects_features:
......
...@@ -91,7 +91,7 @@ Settings.gitlab['signup_enabled'] ||= false ...@@ -91,7 +91,7 @@ Settings.gitlab['signup_enabled'] ||= false
Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
Settings.gitlab['issue_closing_pattern'] = '([Cc]lose[sd]|[Ff]ixe[sd]) #(\d+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['issue_closing_pattern'] = '([Cc]lose[sd]|[Ff]ixe[sd]) (#\d+|([A-Z\-]+-)\d+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil?
Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil?
......
class AddUsernamePasswordApiVersionToServices < ActiveRecord::Migration
def change
add_column :services, :username, :string
add_column :services, :password, :string
add_column :services, :api_version, :string
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20140502125220) do ActiveRecord::Schema.define(version: 20140513095908) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -280,6 +280,9 @@ ActiveRecord::Schema.define(version: 20140502125220) do ...@@ -280,6 +280,9 @@ ActiveRecord::Schema.define(version: 20140502125220) do
t.string "room" t.string "room"
t.text "recipients" t.text "recipients"
t.string "api_key" t.string "api_key"
t.string "username"
t.string "password"
t.string "api_version"
end end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
......
# GitLab JIRA integration
GitLab can be configured to interact with JIRA
*Note* Directions below are for JIRA v6.x and GitLab v6.x
## Configuring JIRA
We need to create a user in JIRA which will have access to all projects that need to integrate with GitLab.
Login to your JIRA instance as admin and under Administration -> User Management create a new user.
For example, let's create user `gitlab`. We've also added `gitlab` user to group `jira-developers` which grants it access to projects.
## Configuring GitLab
In `gitlab.yml` enable [JIRA issue tracker section by uncommenting the lines](https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115).
This will make sure that all issues within GitLab are pointing to the JIRA issue tracker.
We can also enable JIRA service that will allow us to interact with JIRA issues.
For example, we can close issues in JIRA by a commit in GitLab.
Go to project settings page and fill in the project name for the JIRA project, see [screenshot](jira_project_name.png).
Next, go to the services page and find JIRA.
1. Tick the active check box to enable the service.
1. Supply the url to JIRA server, for example http://jira.sample
1. Supply the username of a user we created under `Configuring JIRA` section, for example `gitlab`
1. Supply the password of the user
1. Optional: supply the JIRA api version, default is version 2
1. Save
Now we should be able to interact with JIRA issues, for example we can close a JIRA issue by commiting to our GitLab repository and referencing the JIRA issue( in the format of JIRAPROJECT-123).
For example, for project named NEW we commit with a commit message `Add new file fixes NEW-1` [screenshot](jira_service_commit.png) and that will close an issue NEW-1 in JIRA and add a comment with a link to the commit that closed the issue as shown on the [screenshot](jira_service_close_issue.png)
...@@ -48,3 +48,9 @@ Feature: Project Services ...@@ -48,3 +48,9 @@ Feature: Project Services
And I click email on push service link And I click email on push service link
And I fill email on push settings And I fill email on push settings
Then I should see email on push service settings saved Then I should see email on push service settings saved
Scenario: Activate JIRA service
When I visit project "Shop" services page
And I click jira service link
And I fill jira settings
Then I should see jira service settings saved
...@@ -118,4 +118,23 @@ class ProjectServices < Spinach::FeatureSteps ...@@ -118,4 +118,23 @@ class ProjectServices < Spinach::FeatureSteps
find_field('Room').value.should == '#gitlab' find_field('Room').value.should == '#gitlab'
find_field('Token').value.should == 'verySecret' find_field('Token').value.should == 'verySecret'
end end
step 'I click jira service link' do
click_link 'JIRA'
end
step 'I fill jira settings' do
fill_in 'Project url', with: 'http://jira.example'
fill_in 'Username', with: 'gitlab'
fill_in 'Password', with: 'gitlab'
fill_in 'Api version', with: '2'
click_button 'Save'
end
step 'I should see jira service settings saved' do
find_field('Project url').value.should == 'http://jira.example'
find_field('Username').value.should == 'gitlab'
find_field('Password').value.should_not == 'gitlab'
find_field('Api version').value.should == '2'
end
end end
...@@ -181,7 +181,7 @@ module Gitlab ...@@ -181,7 +181,7 @@ module Gitlab
link_to("##{identifier}", url, html_options.merge(title: "Issue: #{title}", class: "gfm gfm-issue #{html_options[:class]}")) link_to("##{identifier}", url, html_options.merge(title: "Issue: #{title}", class: "gfm gfm-issue #{html_options[:class]}"))
end end
else else
reference_jira_issue(identifier) if @project.issues_tracker == "jira" reference_jira_issue(identifier) if @project.jira_tracker?
end end
end end
......
...@@ -23,9 +23,13 @@ module Gitlab ...@@ -23,9 +23,13 @@ module Gitlab
end end
def issues_for project def issues_for project
issues.map do |identifier| if project.jira_tracker?
project.issues.where(iid: identifier).first issues.uniq
end.reject(&:nil?) else
issues.map do |identifier|
project.issues.where(iid: identifier).first
end.reject(&:nil?)
end
end end
def merge_requests_for project def merge_requests_for project
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# token :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# project_url :string(255)
# subdomain :string(255)
# room :string(255)
# recipients :text
# api_key :string(255)
# username :string(255)
# password :string(255)
# api_version :string(255)
require 'spec_helper'
describe JiraService, models: true do
describe "Associations" do
it { should belong_to :project }
it { should have_one :service_hook }
end
describe "Execute" do
let(:user) { create(:user) }
let(:project) { create(:project) }
before do
@jira_service = JiraService.new
@jira_service.stub(
project_id: project.id,
project: project,
service_hook: true,
project_url: 'http://jira.example.com',
username: 'gitlab_jira_username',
password: 'gitlab_jira_password',
api_version: '2'
)
@sample_data = GitPushService.new.sample_data(project, user)
# https://github.com/bblimke/webmock#request-with-basic-authentication
@api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
WebMock.stub_request(:post, @api_url)
end
it "should call JIRA API" do
@jira_service.execute(@sample_data, "JIRA-123")
WebMock.should have_requested(:post, @api_url).with(
body: /Issue solved with/
).once
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment