diff --git a/CHANGELOG-EE b/CHANGELOG-EE index 7eb1607726..98f8f4d7b3 100644 --- a/CHANGELOG-EE +++ b/CHANGELOG-EE @@ -1,5 +1,6 @@ v 6.9.0 - Support Jira ticket mentions in format JIRA-123 + - Add support for closing Jira tickets with commits and MR v 6.8.0 - Customise sign-in page with custom text and logo diff --git a/app/models/project.rb b/app/models/project.rb index 1da24ee649..336e2cea1a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -64,6 +64,7 @@ class Project < ActiveRecord::Base has_one :assembla_service, dependent: :destroy has_one :gemnasium_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_from_project, through: :forked_project_link # Merge Requests for target project should be removed with it @@ -316,13 +317,17 @@ class Project < ActiveRecord::Base end 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 def gitlab_ci? gitlab_ci_service && gitlab_ci_service.active end + def jira_tracker? + self.issues_tracker == "jira" + end + # For compatibility with old code def code path diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb new file mode 100644 index 0000000000..c1ae4eb250 --- /dev/null +++ b/app/models/project_services/jira_service.rb @@ -0,0 +1,96 @@ +# == 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 diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 715b569075..2f39ce571a 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -88,7 +88,11 @@ class GitPushService if !issues_to_close.empty? && is_default_branch 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 diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 70b4537fd3..46f6dd5257 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -37,6 +37,8 @@ = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder - elsif type == 'checkbox' = f.check_box name + - elsif type == 'password' + = f.password_field name, class: "form-control" .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e4a67be428..b82bec2adf 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -74,7 +74,7 @@ production: &base # 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. # 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_projects_features: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 97f2954640..299dd8cab7 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -91,7 +91,7 @@ Settings.gitlab['signup_enabled'] ||= false 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['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['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? diff --git a/db/migrate/20140513095908_add_username_password_api_version_to_services.rb b/db/migrate/20140513095908_add_username_password_api_version_to_services.rb new file mode 100644 index 0000000000..407c930525 --- /dev/null +++ b/db/migrate/20140513095908_add_username_password_api_version_to_services.rb @@ -0,0 +1,7 @@ +class AddUsernamePasswordApiVersionToServices < ActiveRecord::Migration + def change + add_column :services, :username, :string + add_column :services, :password, :string + add_column :services, :api_version, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index a6f883914e..a8d0708cb0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # 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 enable_extension "plpgsql" @@ -280,6 +280,9 @@ ActiveRecord::Schema.define(version: 20140502125220) do t.string "room" t.text "recipients" t.string "api_key" + t.string "username" + t.string "password" + t.string "api_version" end add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree diff --git a/doc/integration/jira.md b/doc/integration/jira.md new file mode 100644 index 0000000000..8740dfe6b7 --- /dev/null +++ b/doc/integration/jira.md @@ -0,0 +1,35 @@ +# 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) + diff --git a/doc/integration/jira_project_name.png b/doc/integration/jira_project_name.png new file mode 100644 index 0000000000..5986fdb63f Binary files /dev/null and b/doc/integration/jira_project_name.png differ diff --git a/doc/integration/jira_service.png b/doc/integration/jira_service.png new file mode 100644 index 0000000000..fff2769246 Binary files /dev/null and b/doc/integration/jira_service.png differ diff --git a/doc/integration/jira_service_close_issue.png b/doc/integration/jira_service_close_issue.png new file mode 100644 index 0000000000..b21035d642 Binary files /dev/null and b/doc/integration/jira_service_close_issue.png differ diff --git a/doc/integration/jira_service_commit.png b/doc/integration/jira_service_commit.png new file mode 100644 index 0000000000..a24875d7dc Binary files /dev/null and b/doc/integration/jira_service_commit.png differ diff --git a/features/project/service.feature b/features/project/service.feature index a5af065c9e..0faca6691f 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -48,3 +48,9 @@ Feature: Project Services And I click email on push service link And I fill email on push settings 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 diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 0594a08a5e..5abd588ab5 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -118,4 +118,23 @@ class ProjectServices < Spinach::FeatureSteps find_field('Room').value.should == '#gitlab' find_field('Token').value.should == 'verySecret' 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 diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index dca3d7a7be..5e2fadee42 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -181,7 +181,7 @@ module Gitlab link_to("##{identifier}", url, html_options.merge(title: "Issue: #{title}", class: "gfm gfm-issue #{html_options[:class]}")) end else - reference_jira_issue(identifier) if @project.issues_tracker == "jira" + reference_jira_issue(identifier) if @project.jira_tracker? end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 94b01e808d..a95ff0b5a5 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -23,9 +23,13 @@ module Gitlab end def issues_for project - issues.map do |identifier| - project.issues.where(iid: identifier).first - end.reject(&:nil?) + if project.jira_tracker? + issues.uniq + else + issues.map do |identifier| + project.issues.where(iid: identifier).first + end.reject(&:nil?) + end end def merge_requests_for project diff --git a/spec/models/jira_service_spec.rb b/spec/models/jira_service_spec.rb new file mode 100644 index 0000000000..6dfcee323f --- /dev/null +++ b/spec/models/jira_service_spec.rb @@ -0,0 +1,59 @@ +# == 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